/**
* Mastodon embed timeline
* @author idotj
* @version 4.7.0
* @url https://gitlab.com/idotj/mastodon-embed-timeline
* @license GNU AGPLv3
*/
class t {
constructor(t = {}) {
((this.defaultSettings = {
mtContainerId: "mt-container",
instanceUrl: "https://mastodon.social",
timelineType: "local",
userId: "",
profileName: "",
hashtagName: "",
spinnerClass: "mt-loading-spinner",
defaultTheme: "auto",
maxNbPostFetch: "20",
maxNbPostShow: "20",
dateFormatLocale: "en-GB",
dateFormatOptions: {
day: "2-digit",
month: "short",
year: "numeric",
},
hideUnlisted: !1,
hideReblog: !1,
hideReplies: !1,
hidePinnedPosts: !1,
hideUserAccount: !1,
txtMaxLines: "",
filterByLanguage: "",
btnShowMore: "SHOW MORE",
btnShowLess: "SHOW LESS",
markdownBlockquote: !1,
hideEmojos: !1,
btnShowContent: "SHOW CONTENT",
hideVideoPreview: !1,
btnPlayVideoTxt: "Load and play video",
hidePreviewLink: !1,
previewMaxLines: "",
hideCounterBar: !1,
disableCarousel: !1,
carouselCloseTxt: "Close carousel",
carouselPrevTxt: "Previous media item",
carouselNextTxt: "Next media item",
btnSeeMore: "See more posts at Mastodon",
btnReload: "Refresh",
insistSearchContainer: !1,
insistSearchContainerTime: "3000",
}),
(this.mtSettings = { ...this.defaultSettings, ...t }),
this.#t(),
(this.linkHeader = {}),
(this.mtContainerNode = ""),
(this.mtBodyNode = ""),
(this.fetchedData = {}),
this.#e(() => {
this.#i();
}));
}
#t() {
Number(this.mtSettings.maxNbPostShow) >
Number(this.mtSettings.maxNbPostFetch) &&
(console.error(
`Please check your settings! The maximum number of posts to show is bigger than the maximum number of posts to fetch. Changing the value of "maxNbPostFetch" to: ${this.mtSettings.maxNbPostShow}`,
),
(this.mtSettings.maxNbPostFetch = this.mtSettings.maxNbPostShow));
}
#e(t) {
"undefined" != typeof document && "complete" === document.readyState
? t()
: "undefined" != typeof document &&
"complete" !== document.readyState &&
document.addEventListener("DOMContentLoaded", t());
}
#i() {
const t = () => {
((this.mtContainerNode = document.getElementById(
this.mtSettings.mtContainerId,
)),
(this.mtBodyNode =
this.mtContainerNode.getElementsByClassName("mt-body")[0]),
this.#s(),
this.#a("newTimeline"));
};
if (this.mtSettings.insistSearchContainer) {
const e = performance.now(),
i = () => {
if (document.getElementById(this.mtSettings.mtContainerId))
t();
else {
performance.now() - e <
this.mtSettings.insistSearchContainerTime
? requestAnimationFrame(i)
: console.error(
`Impossible to find the
container with id: "${this.mtSettings.mtContainerId}" after several attempts for ${this.mtSettings.insistSearchContainerTime / 1e3} seconds`,
);
}
};
i();
} else
document.getElementById(this.mtSettings.mtContainerId)
? t()
: console.error(
`Impossible to find the
container with id: "${this.mtSettings.mtContainerId}". Please try to add the option 'insistSearchContainer: true' when initializing the script`,
);
}
mtUpdate() {
this.#e(() => {
(this.mtBodyNode.replaceChildren(),
this.mtBodyNode.insertAdjacentHTML(
"afterbegin",
'',
),
this.#a("updateTimeline"));
});
}
mtColorTheme(t) {
this.#e(() => {
this.mtContainerNode.setAttribute("data-theme", t);
});
}
#s() {
if ("auto" === this.mtSettings.defaultTheme) {
let t = window.matchMedia("(prefers-color-scheme: dark)");
(t.matches ? this.mtColorTheme("dark") : this.mtColorTheme("light"),
t.addEventListener("change", (t) => {
t.matches
? this.mtColorTheme("dark")
: this.mtColorTheme("light");
}));
} else this.mtColorTheme(this.mtSettings.defaultTheme);
}
#o() {
return new Promise((t, e) => {
const i = this.mtSettings.instanceUrl
? `${this.mtSettings.instanceUrl}/api/v1/`
: this.#n(
"Please check your instanceUrl value",
"⚠️",
),
s = this.#r(i),
a = Object.entries(s).map(([t, i]) =>
this.#l(i, t)
.then((e) => ({ [t]: e }))
.catch(
(i) => (
e(
new Error(
"Something went wrong getting the timeline data.",
),
),
this.#n(i.message),
{ [t]: [] }
),
),
);
Promise.all(a).then(async (e) => {
if (
((this.fetchedData = e.reduce(
(t, e) => ({ ...t, ...e }),
{},
)),
!this.mtSettings.hidePinnedPosts &&
void 0 !== this.fetchedData.pinned?.length &&
0 !== this.fetchedData.pinned.length)
) {
const t = this.fetchedData.pinned.map((t) => ({
...t,
pinned: !0,
}));
this.fetchedData.timeline = [
...t,
...this.fetchedData.timeline,
];
}
if (this.#d()) t();
else {
do {
await this.#m();
} while (!this.#d() && this.linkHeader.next);
t();
}
});
});
}
#r(t) {
const {
timelineType: e,
userId: i,
hashtagName: s,
hideReblog: a,
hideReplies: o,
maxNbPostFetch: n,
hidePinnedPosts: r,
hideEmojos: l,
} = this.mtSettings,
d = {};
switch (e) {
case "profile":
if (!i) {
this.#n(
"Please check your userId value",
"⚠️",
);
break;
}
((d.timeline = `${t}accounts/${i}/statuses?limit=${n}`),
r || (d.pinned = `${t}accounts/${i}/statuses?pinned=true`));
break;
case "hashtag":
if (!s) {
this.#n(
"Please check your hashtagName value",
"⚠️",
);
break;
}
d.timeline = `${t}timelines/tag/${s}?limit=${n}`;
break;
case "local":
d.timeline = `${t}timelines/public?local=true&limit=${n}`;
break;
default:
this.#n(
"Please check your timelineType value",
"⚠️",
);
}
return (
a && (d.timeline += "&exclude_reblogs=true"),
o && (d.timeline += "&exclude_replies=true"),
l || (d.emojos = `${t}custom_emojis`),
d
);
}
async #l(t, e) {
const i = await fetch(t);
if (!i.ok)
throw new Error(
`\n Failed to fetch the following Url: ${t}Error status: ${i.status}Error message: ${i.statusText}\n `,
);
const s = await i.json();
return (
"timeline" === e &&
i.headers.get("Link") &&
(this.linkHeader = this.#h(i.headers.get("Link"))),
s
);
}
#d() {
return (
this.fetchedData.timeline.length >=
Number(this.mtSettings.maxNbPostFetch)
);
}
#m() {
return new Promise((t) => {
this.linkHeader.next
? this.#l(this.linkHeader.next, "timeline")
.then((e) => {
((this.fetchedData.timeline = [
...this.fetchedData.timeline,
...e,
]),
t());
})
.catch(
(t) => (
reject(
new Error(
"Something went wrong fetching more posts.",
),
),
this.#n(t.message),
{ [key]: [] }
),
)
: t();
});
}
#h(t) {
const e = t
.split(", ")
.map((t) => t.split("; "))
.map((t) => [
t[1].replace(/"/g, "").replace("rel=", ""),
t[0].slice(1, -1),
]);
return Object.fromEntries(e);
}
async #a(t) {
await this.#o();
const {
hideUnlisted: e,
maxNbPostShow: i,
filterByLanguage: s,
} = this.mtSettings,
a = this.fetchedData.timeline;
this.mtBodyNode.replaceChildren();
if (
(a
.filter((t) => {
const i =
"public" === t.visibility ||
(!e && "unlisted" === t.visibility),
a = t.language || (t.reblog ? t.reblog.language : null);
return i && ("" === s || a === s);
})
.forEach((t, e) => {
e < i && this.#c(t, e);
}),
"" !== this.mtBodyNode.innerHTML)
)
"newTimeline" === t
? (this.#p(),
this.#g(),
this.#u(0),
this.#v(),
(this.mtSettings.btnSeeMore || this.mtSettings.btnReload) &&
this.#b())
: "updateTimeline" === t
? this.#p()
: this.#n(
"The function buildTimeline() was expecting a param",
);
else {
const t = `No posts to show${a?.length || 0} posts have been fetched from the serverThis may be due to an incorrect configuration with the parameters or with the filters applied (to hide certains type of posts)`;
this.#n(t, "📭");
}
}
#g() {
("0" !== this.mtSettings.txtMaxLines &&
0 !== this.mtSettings.txtMaxLines.length &&
this.mtBodyNode.parentNode.style.setProperty(
"--mt-txt-max-lines",
this.mtSettings.txtMaxLines,
),
"0" !== this.mtSettings.previewMaxLines &&
0 !== this.mtSettings.previewMaxLines.length &&
this.mtBodyNode.parentNode.style.setProperty(
"--mt-preview-max-lines",
this.mtSettings.previewMaxLines,
));
}
#u(t) {
const e = this.mtBodyNode.getElementsByTagName("article");
for (let i = 0; i < t; i++) e[i].setAttribute("aria-setsize", t);
}
#c(t, e) {
this.mtBodyNode.insertAdjacentHTML("beforeend", this.#w(t, e));
}
#w(t, e) {
const i = Boolean(t.reblog),
s = i ? t.reblog : t,
{
url: a,
created_at: o,
replies_count: n,
reblogs_count: r,
favourites_count: l,
} = s,
{
avatar: d,
url: m,
username: h,
display_name: c,
emojis: p,
} = s.account,
g =
'
`;
}
#M(t, e) {
function i(t, e) {
let i = e.replace(/\s+/g, "").toLowerCase();
return (
!(
!["src", "href", "xlink:href"].includes(t) ||
(!i.includes("javascript:") && !i.includes("data:"))
) ||
!!t.startsWith("on") ||
void 0
);
}
function s(t) {
let e = t.attributes;
for (let { name: s, value: a } of e)
i(s, a) && t.removeAttribute(s);
}
let a =
new DOMParser().parseFromString(t, "text/html").body ||
document.createElement("body");
return (
(function (t) {
let e = t.querySelectorAll("script");
for (let t of e) t.remove();
})(a),
(function t(e) {
let i = e.children;
for (let e of i) (s(e), t(e));
})(a),
e ? a.childNodes : a.innerHTML
);
}
#L(t) {
let e = t;
return (
(e = this.#M(e, !1)),
(e = this.#k(e)),
this.mtSettings.hideEmojos ||
(e = this.#x(e, this.fetchedData.emojos)),
this.mtSettings.markdownBlockquote &&
(e = this.#C(
e,
"
>",
"
",
"
",
"
",
)),
e
);
}
#k(t) {
let e = t.replaceAll('rel="tag"', 'rel="tag" target="_blank"');
return (
(e = e.replaceAll(
'class="u-url mention"',
'class="u-url mention" target="_blank"',
)),
e
);
}
#C(t, e, i, s, a) {
if (t.includes(e)) {
const o = new RegExp(e + "(.*?)" + i, "gi");
return t.replace(o, s + "$1" + a);
}
return t;
}
#f(t) {
return (t ?? "")
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
#x(t, e) {
if (t.includes(":")) {
for (const i of e) {
const e = new RegExp(`\\:${i.shortcode}\\:`, "g");
t = t.replace(
e,
``,
);
}
return t;
}
return t;
}
#S(t) {
const e = new Date(t);
return new Intl.DateTimeFormat(
this.mtSettings.dateFormatLocale,
this.mtSettings.dateFormatOptions,
).format(e);
}
#T(t, e = !1) {
const { type: i, url: s, preview_url: a, description: o, meta: n } = t,
{ original: r, small: l } = n,
{
spinnerClass: d,
btnShowContent: m,
btnPlayVideoTxt: h,
hideVideoPreview: c,
} = this.mtSettings,
p =
'",
g =
' class="mt-post-media ' +
(e ? "mt-post-media-spoiler " : "") +
(d || "") +
'" data-media-type="' +
i +
'" data-media-url-hd="' +
s +
'"' +
(o ? ' data-media-alt-txt="' + this.#f(o) + '"' : "") +
' data-media-width-hd="' +
r.width +
'" data-media-height-hd="' +
r.height +
'" style="padding-top: calc(100%/' +
l?.aspect +
')">';
return "image" === i
? "
'
: "audio" === i
? '
' +
(e ? p : "") +
'' +
(a
? ''
: "") +
"
"
: "video" === i || "gifv" === i
? c
? "'
: "
'
: "";
}
#P(t, e) {
let i = document.createElement("dialog");
((i.id = t),
i.classList.add("mt-dialog"),
(i.dataset.theme = this.mtContainerNode.getAttribute("data-theme")),
(i.innerHTML = e),
document.body.prepend(i),
i.showModal(),
i.addEventListener("close", () => {
document.body.removeChild(i);
}));
}
#E(t) {
const e = Array.from(t.target.parentNode.parentNode.children).filter(
(t) => !t.classList.contains("mt-post-media-spoiler"),
),
i = e.indexOf(t.target.parentNode) + 1;
let s = [];
e.forEach((t, e) => {
let i = "";
i =
"gifv" === t.getAttribute("data-media-type") ||
"video" === t.getAttribute("data-media-type")
? `\n \n `
: `\n \n `;
const a = `\n
\n
\n ${i}\n
\n
\n `;
s.push(a);
});
const a = `\n
\n \n
\n\n
\n
\n ${s.join("")}\n
\n
\n\n \n\n \n `;
(this.#P("mt-carousel", a), s.length >= 2 && this.#$(e.length, i));
}
#$(t, e) {
let i = e;
const s = document.getElementById("mt-carousel-scroll");
let a = 0,
o = !1;
const n = document.getElementById("mt-carousel-prev"),
r = document.getElementById("mt-carousel-next"),
l = (t, e = "smooth") => {
document
.getElementById("mt-carousel-" + t)
.scrollIntoView({ behavior: e });
};
l(i, "instant");
const d = () => {
(clearTimeout(a),
(a = setTimeout(() => {
(o &&
((i = (() => {
const t =
(s.scrollLeft + s.clientWidth) / s.clientWidth;
return Math.round(t + Number.EPSILON);
})()),
m()),
(o = !0));
}, 60)));
};
s.addEventListener("scroll", d);
const m = () => {
((n.hidden = 1 === i), (r.hidden = i === t));
},
h = (e) => {
const s = e.target.closest("button")?.id;
("mt-carousel-next" === s
? ((o = !1), ++i, i > t && (i = t), l(i), m())
: "mt-carousel-prev" === s &&
((o = !1), --i, i < 1 && (i = 1), l(i), m()),
"mt-carousel-close" === s && p());
};
document.addEventListener("click", h);
const c = (t) => {
("Escape" !== t.key && 27 !== t.keyCode) || p();
};
document.addEventListener("keydown", c);
const p = () => {
(s.removeEventListener("scroll", d),
document.removeEventListener("click", h),
document.removeEventListener("keydown", c));
};
}
#B(t) {
const e = t.target.closest("[data-media-type]"),
i = e.dataset.mediaUrlHd;
(e.replaceChildren(),
(e.innerHTML = ``));
}
#A(t) {
const e = t.target,
i = e.nextSibling,
s = "true" === e.getAttribute("aria-expanded");
(i.classList.toggle("spoiler-txt-hidden", s),
i.classList.toggle("spoiler-txt-visible", !s),
e.setAttribute("aria-expanded", !s),
(e.textContent = s
? this.mtSettings.btnShowMore
: this.mtSettings.btnShowLess));
}
#H(t) {
const e = t.target;
e.parentNode.classList.toggle(
"mt-post-media-spoiler",
!e.classList.contains("mt-btn-spoiler-media-show"),
);
}
#y(t) {
const {
url: e,
image: i,
image_description: s,
provider_name: a,
title: o,
description: n,
author_name: r,
} = t,
{ previewMaxLines: l, spinnerClass: d } = this.mtSettings;
let m = "";
return (
"0" !== l &&
n &&
(m =
'' +
this.#q(n) +
""),
'' +
(i
? '
"
);
}
#q(t) {
return new DOMParser().parseFromString(t, "text/html").body.textContent;
}
#b() {
const {
btnSeeMore: t,
btnReload: e,
timelineType: i,
profileName: s,
hashtagName: a,
instanceUrl: o,
} = this.mtSettings;
let n = "",
r = "";
if (t) {
let e = "";
switch (i) {
case "profile":
s
? (e = s)
: this.#n(
"Please check your profileName value",
"⚠️",
);
break;
case "hashtag":
e = "tags/" + a;
break;
case "local":
e = "public/local";
}
n =
'' +
t +
"";
}
if (
(e &&
(r =
'"),
this.mtBodyNode.parentNode.insertAdjacentHTML(
"beforeend",
'",
),
e)
) {
const t = this.mtContainerNode.querySelector(".btn-refresh");
t && t.addEventListener("click", () => this.mtUpdate());
}
}
#v() {
(this.mtBodyNode.addEventListener("click", (t) => {
const e = t.target,
i = e.localName,
s = e.parentNode;
(("article" == i ||
"article" == e.offsetParent?.localName ||
(this.mtSettings.disableCarousel &&
"image" === s.getAttribute("data-media-type"))) &&
this.#D(t),
e.classList.contains("mt-btn-spoiler-txt") && this.#A(t),
e.classList.contains("mt-btn-spoiler-media") && this.#H(t),
this.mtSettings.disableCarousel ||
"img" != i ||
("image" !== s.getAttribute("data-media-type") &&
"audio" !== s.getAttribute("data-media-type")) ||
this.#E(t),
("mt-btn-play" == e.className ||
("svg" == i && "mt-btn-play" == s.className) ||
("path" == i && "mt-btn-play" == s.parentNode.className) ||
("img" == i &&
("video" === s.getAttribute("data-media-type") ||
"gifv" === s.getAttribute("data-media-type")))) &&
this.#B(t));
}),
this.mtBodyNode.addEventListener("keydown", (t) => {
const e = t.target.localName;
"Enter" === t.key && "article" == e && this.#D(t);
}));
}
#D(t) {
const e = t.target.closest(".mt-post")?.dataset.location;
if (!e) return;
const i = t.target.localName;
if (
"a" === i ||
"span" === i ||
"button" === i ||
"bdi" === i ||
"time" === i
)
return;
const s = t.target.className;
if ("mt-post-media-spoiler" === s || "mt-post-preview-noImage" === s)
return;
const a = t.target.parentNode?.className;
"mt-post-avatar-image-big" !== a &&
"mt-post-avatar-image-small" !== a &&
"mt-post-header-user-name" !== a &&
"mt-post-preview-image" !== a &&
"mt-post-preview" !== a &&
window.open(e, "_blank", "noopener");
}
#p() {
const t = (e) => {
(e.target.parentNode.classList.remove(this.mtSettings.spinnerClass),
e.target.removeEventListener("load", t),
e.target.removeEventListener("error", t));
};
this.mtBodyNode
.querySelectorAll(`.${this.mtSettings.spinnerClass} > img`)
.forEach((e) => {
(e.addEventListener("load", t), e.addEventListener("error", t));
});
}
#n(t, e) {
const i = e || "❌";
throw (
(this.mtBodyNode.innerHTML = `\n
\n ${i}\n Oops, something's happened:\n
${t}
\n
`),
this.mtBodyNode.setAttribute("role", "none"),
new Error(
"Stopping the script due to an error building the timeline.",
)
);
}
}
export { t as Init };