/** * 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 server
This 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 = '
' +
                this.#f(h) +
                ' avatar
' + (i ? '
' +
                      this.#f(t.account.username) +
                      ' avatar
' : "") + "
", u = '
' + (!this.mtSettings.hideEmojos && c ? this.#x(c, p) : c || h) + "" + (this.mtSettings.hideUserAccount ? "" : '
") + "
", v = this.#S(o), b = '
' + (t.pinned ? '' : "") + '" + (t.edited_at ? " *" : "") + "
", w = this.mtSettings.hidePreviewLink || (!t.card && !t.reblog?.card) ? "" : this.#y(i ? t.reblog.card : t.card), f = "0" !== this.mtSettings.txtMaxLines && this.mtSettings.txtMaxLines.length ? " truncate" : ""; let x = ""; const S = s.spoiler_text ? s.spoiler_text : s.content, y = '
' + this.#L(s.content) + w + "
"; S && (x = '
' + this.#L(S) + (s.spoiler_text ? y : "") + "
"); const L = [ ...t.media_attachments, ...(t.reblog?.media_attachments || []), ] .map((t) => this.#T(t, s.sensitive)) .join(""), T = L ? `
${L}
` : "", N = t.poll ? '
    ' + t.poll.options .map(function (t) { return "
  • " + t.title + "
  • "; }) .join("") + "
" : "", M = this.mtSettings.hideCounterBar ? "" : '
' + this.#N("replies", n) + this.#N("reblog", r) + this.#N("favorites", l) + "
"; return ( '
' + g + u + b + "
" + x + T + N + (s.spoiler_text ? "" : w) + M + "
" ); } #N(t, e) { return `
${{ replies: '', reblog: '', favorites: '' }[t]}${e}
`; } #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, `Emoji ${i.shortcode}`, ); } 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 ? '' +
                      (o ? this.#f(o) : ' : "") + "
" : "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 ${t.getAttribute(\n `; const a = `\n \n `; s.push(a); }); const a = `\n \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 ? '
' +
                      this.#f(s) +
                      '
' : '
📄
') + '
' + (a ? '' + this.#q(a) + "" : "") + '' + o + "" + m + (r ? '' + this.#q(r) + "" : "") + "
" ); } #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 };