/** * @file Supporting classes for leftychan javascript. * @author jonsmy */ globalThis.LCNSite = class LCNSite { static "createAbortable" () { const obj = { "abort": null, "controller": null, "signal": null } const setupController = () => { obj.controller = new AbortController() obj.signal = obj.controller.signal } obj.abort = () => { obj.controller.abort() setupController() } setupController() return obj } static "getThreadFromPages" (pages, thread_id) { for (const page of pages) { for (const thread of page.threads) { if (thread_id == String(thread.no)) { return { "page": page.page, ...thread } } } } return null }; constructor () { this._isModerator = document.body.classList.contains("is-moderator"); this._isThreadPage = document.body.classList.contains("active-thread"); this._isBoardPage = document.body.classList.contains("active-board"); this._isCatalogPage = document.body.classList.contains("active-catalog"); this._isModPage = location.pathname == "/mod.php"; this._isModRecentsPage = this._isModPage && (location.search == "?/recent" || location.search.startsWith("?/recent/")); this._isModReportsPage = this._isModPage && (location.search == "?/reports" || location.search.startsWith("?/reports/")); this._isModLogPage = this._isModPage && (location.search == "?/log" || location.search.startsWith("?/log/")); this._unseen = 0; this._pageTitle = document.title; this._favicon = document.querySelector("head > link[rel=\"shortcut icon\"]"); this._generatedStyle = null; } "_doTitleUpdate" () { document.title = (this._unseen > 0 ? `(${this._unseen}) ` : "") + this._pageTitle; }; "isModerator" () { return this._isModerator; } "isThreadPage" () { return this._isThreadPage; } "isBoardPage" () { return this._isBoardPage; } "isCatalogPage" () { return this._isCatalogPage; } "isModPage" () { return this._isModPage; } "isModRecentsPage" () { return this._isModRecentsPage; } "isModReportsPage" () { return this._isModReportsPage; } "isModLogPage" () { return this._isModLogPage; } "getUnseen" () { return this._unseen; } "clearUnseen" () { if (this._unseen != 0) { this.setUnseen(0); } } "setUnseen" (int) { const bool = !!int if (bool != !!this._unseen) { this.setFaviconType(bool ? "reply" : null) } this._unseen = int this._doTitleUpdate() } "getTitle" () { return this._pageTitle; } "setTitle" (title) { this._pageTitle = title; this._doTitleUpdate(); } "setFaviconType" (type=null) { if (this._favicon == null) { this._favicon = document.createElement("link") this._favicon.rel = "shortcut icon" document.head.appendChild(this._favicon) } this._favicon.href = `/favicon${type ? "-" + type : ""}.ico` } "getFloaterLContainer" () { return document.getElementById("bar-bottom-l"); } "getFloaterRContainer" () { return document.getElementById("bar-bottom-r"); } "getThreadStatsLContainer" () { return document.getElementById("lcn-threadstats-l"); } "getThreadStatsRContainer" () { return document.getElementById("lcn-threadstats-r"); } "writeCSSStyle" (origin, stylesheet) { if (this._generatedStyle == null && (this._generatedStyle = document.querySelector("head > style.generated-css")) == null) { this._generatedStyle = document.createElement("style") this._generatedStyle.classList.add("generated-css") document.head.appendChild(this._generatedStyle) } this._generatedStyle.textContent += `${this._generatedStyle.textContent.length ? "\n\n" : ""}/*** Generated by ${origin} ***/\n${stylesheet}` } } globalThis.LCNSite.INSTANCE = null; globalThis.LCNPostInfo = class LCNPostInfo { constructor () { this._boardId = null; this._threadId = null; this._postId = null; this._name = null; this._email = null; this._capcode = null; this._flag = null; this._ip = null; this._subject = null; this._createdAt = null; this._parent = null; this._isThread = false; this._isReply = false; this._isLocked = false; this._isSticky = false; } static "assign" (post) { if (post[this.nodeAttrib] == null) { return post[this.nodeAttrib] = this.from(post); } else { return post[this.nodeAttrib]; } } static "from" (post) { assert.ok(post.classList.contains("post"), "Arty must be expected Element."); const inst = new this(); const intro = post.querySelector(".intro"); const link = intro.querySelector(".post_no:not([id])").href.split("/").reverse(); inst._postId = link[0].slice(link[0].indexOf("#q") + 2); inst._threadId = link[0].slice(0, link[0].indexOf(".")); inst._boardId = link[2]; inst._isThread = post.classList.contains("op"); inst._isReply = !inst._isThread; inst._subject = cont(intro.querySelector(".subject"), x => x.innerText); inst._name = cont(intro.querySelector(".name"), x => x.innerText); inst._email = cont(intro.querySelector(".email"), x => x.href.slice(7)); inst._flag = cont(intro.querySelector(".flag"), x => x.src.split("/").reverse()[0].slice(0, -4)); inst._capcode = cont(intro.querySelector(".capcode"), x => x.innerText); inst._ip = cont(intro.querySelector(".ip-link"), x => x.innerText); inst._createdAt = new Date(intro.querySelector("time[datetime]").dateTime || NaN); inst._isSticky = !!intro.querySelector("i.fa-thumb-tack"); inst._isLocked = !!intro.querySelector("i.fa-lock"); return inst; } "getParent" () { return this._parent; } "__setParent" (inst) { return this._parent = inst; } "getBoardId" () { return this._boardId; } "getThreadId" () { return this._threadId; } "getPostId" () { return this._postId; } "getHref" () { return `/${this.boardId}/res/${this.threadId}.html#q${this.postId}`; } "getName" () { return this._name; } "getEmail" () { return this._email; } "getIP" () { return this._ip; } "getCapcode" () { return this._capcode; } "getSubject" () { return this._subject; } "getCreatedAt" () { return this._createdAt; } "isSticky" () { return this._isSticky; } "isLocked" () { return this._isLocked; } "isThread" () { return this._isThread; } "isReply" () { return this._isReply; } "is" (info) { assert.ok(info, "Must be LCNPost.") return this.getBoardId() == info.getBoardId() && this.getPostId() == info.getPostId() } } LCNPostInfo.nodeAttrib = "$LCNPostInfo"; LCNPostInfo.selector = ".post:not(.grid-li)"; globalThis.LCNPost = class LCNPost { static "assign" (post) { return post[this.nodeAttrib] || (post[this.nodeAttrib] = this.from(post)); } static "from" (post) { return new this(post); } constructor (post) { this._parent = null; this._post = null; this._info = null; this._ipLink = null; this._controls = null; this._customControlsSeperatorNode = null; assert.ok(post.classList.contains("post"), "Arty must be expected Element."); const intro = post.querySelector(".intro"); this._post = post; this._info = LCNPostInfo.assign(post); this._ipLink = intro.querySelector(".ip-link"); this._controls = Array.prototype.at.apply(post.querySelectorAll(".controls"), [ -1 ]); //assert.equal(this._info.getParent(), null, "Info should not have parent."); this._info.__setParent(this); } "jQuery" () { return $(this._post); } "trigger" (event_id, data=null) { $(this._post).trigger(event_id, [ data ]); } "getElement" () { return this._post; } "getInfo" () { return this._info; } "getIPLink" () { return this._ipLink; } "setIP" (ip) { this._ipLink.innerText = ip; } "getParent" () { return this._parent; } "__setParent" (inst) { return this._parent = inst; } "addCustomControl" (obj) { if (LCNSite.INSTANCE.isModerator()) { const link = document.createElement("a") link.innerText = `[${obj.btn}]` link.title = obj.tooltip if (typeof obj.href == "string") { link.href = obj.href link.referrerPolicy = "no-referrer" } else if (obj.onClick != undefined) { link.style.cursor = "pointer" link.addEventListener("click", e => { e.preventDefault(); obj.onClick(this); }) } if (this._customControlsSeperatorNode == null) { this._controls.insertBefore(this._customControlsSeperatorNode = new Text(`${this.constructor.NBSP}-${this.constructor.NBSP}`), this._controls.firstElementChild) } else { this._controls.insertBefore(new Text(this.constructor.NBSP), this._customControlsSeperatorNode) } this._controls.insertBefore(link, this._customControlsSeperatorNode) } } } globalThis.LCNPost.nodeAttrib = "$LCNPost"; globalThis.LCNPost.selector = ".post:not(.grid-li)"; globalThis.LCNPost. NBSP = String.fromCharCode(160); globalThis.LCNThread = class LCNThread { static "assign" (thread) { return thread[this.nodeAttrib] || (thread[this.nodeAttrib] = this.from(thread)); } static "from" (thread) { return new this(thread); } constructor (thread) { this._element = null; this._parent = null; this._op = null; assert.ok(thread.classList.contains("thread"), "Arty must be expected Element.") this._element = thread this._op = LCNPost.assign(this._element.querySelector(".post.op")) assert.equal(this._op.getParent(), null, "Op should not have parent.") this._op.__setParent(this) } "getElement" () { return this._element; } "getContent" () { return this._op; } "getPosts" () { return Array.prototype.map.apply(this._element.querySelectorAll(".post"), [ el => LCNPost.assign(el) ]); } "getReplies" () { return Array.prototype.map.apply(this._element.querySelectorAll(".post:not(.op)"), [ el => LCNPost.assign(el) ]); } "getParent" () { return this._parent; } "__setParent" (inst) { return this._parent = inst; } } globalThis.LCNThread.nodeAttrib = "$LCNThread"; globalThis.LCNThread.selector = ".thread:not(.grid-li)"; globalThis.LCNPostContainer = class LCNPostContainer { static "assign" (container) { return container[this.nodeAttrib] || (container[this.nodeAttrib] = this.from(container)); } static "from" (container) { return new this(container); } constructor (container) { this._parent = null; this._element = null; this._content = null; this._postId = null; this._boardId = null; assert.ok(container.classList.contains("postcontainer"), "Arty must be expected Element.") const child = container.querySelector(".thread, .post") this._element = container this._content = child.classList.contains("thread") ? LCNThread.assign(child) : LCNPost.assign(child) this._boardId = container.dataset.board this._postId = container.id.slice(2) assert.equal(this._content.getParent(), null, "Content should not have parent.") this._content.__setParent(this) } "getElement" () { return this._element; } "getContent" () { return this._content; } "getBoardId" () { return this._boardId; } "getPostId" () { return this._postId; } "getParent" () { return this._parent; } "__setParent" (inst) { return this._parent = inst; } } globalThis.LCNPostContainer.nodeAttrib = "$LCNPostContainer"; globalThis.LCNPostContainer.selector = ".postcontainer"; globalThis.LCNPostWrapper = class LCNPostWrapper { static "assign" (wrapper) { return wrapper[this.nodeAttrib] || (wrapper[this.nodeAttrib] = this.from(wrapper)); } static "from" (wrapper) { return new this(wrapper); } constructor (wrapper) { this._wrapper = null; this._eitaLink = null; this._eitaId = null; this._eitaHref = null this._content = null; assert.ok(wrapper.classList.contains("post-wrapper"), "Arty must be expected Element.") this._wrapper = wrapper this._eitaLink = wrapper.querySelector(".eita-link") this._eitaId = this._eitaLink.id this._eitaHref = this._eitaLink.href void Array.prototype.find.apply(wrapper.children, [ el => { if (el.classList.contains("thread")) { return this._content = LCNThread.assign(el) } else if (el.classList.contains("postcontainer")) { return this._content = LCNPostContainer.assign(el) } } ]) assert.ok(this._content, "Wrapper should contain content.") assert.equal(this._content.getParent(), null, "Content should not have parent.") this._content.__setParent(this) } "getPost" () { const post = this.getContent().getContent() assert.ok(post instanceof LCNPost, "Post should be LCNPost.") return post } "getElement" () { return this._wrapper; } "getContent" () { return this._content; } "getEitaId" () { return this._eitaId; } "getEitaHref" () { return this._eitaHref; } "getEitaLink" () { return this._eitaLink; } } globalThis.LCNPostWrapper.nodeAttrib = "$LCNPostWrapper"; globalThis.LCNPostWrapper.selector = ".post-wrapper"; globalThis.LCNSetting = class LCNSetting { static "build" (id) { return new this(id); } constructor (id) { this._id = null; this._eventId = null; this._label = null; this._hidden = false; this._value = null; this._valueDefault = null; this._id = id; this._eventId = `lcnsetting::${this._id}` } "_getValue" () { const v = localStorage.getItem(this._id) if (v != null) { return this.__builtinValueImporter(v) } else { return this._valueDefault } } "isHidden" () { return this._hidden; } "setHidden" (v) { this._hidden = v; return this; } "getValue" () { return this._value || (this._value = this._getValue()); } "setValue" (v) { if (this._value !== v) { this._value = v localStorage.setItem(this._id, this.__builtinValueExporter(this._value)) setTimeout(() => $(document).trigger(`${this._eventId}::change`, [ v, this ]), 1) } } "getLabel" () { return this._label; } "setLabel" (label) { this._label = label; return this; } "getDefaultValue" () { return this._valueDefault; } "setDefaultValue" (vd) { this._valueDefault = vd; return this; } "onChange" (fn) { $(document).on(`${this._eventId}::change`, (_,v,i) => fn(v, i)); } __setIdPrefix (prefix) { this._id = `${prefix}_${this._id}` this._eventId = `lcnsetting::${this._id}` } } globalThis.LCNToggleSetting = class LCNToggleSetting extends LCNSetting { __builtinValueImporter (v) { return v == "1"; } __builtinValueExporter (v) { return v ? "1" : ""; } __builtinDOMConstructor () { const div = document.createElement("div") const chk = document.createElement("input") const lbl = document.createElement("label") const id = `lcnts::${this.id}` lbl.htmlFor = id lbl.innerText = this.getLabel() chk.id = id chk.type = "checkbox" chk.checked = this.getValue() chk.addEventListener("click", e => { e.preventDefault(); this.setValue(!this.getValue()) }) this.onChange(v => chk.checked = v) div.appendChild(chk) div.appendChild(lbl) return div } } globalThis.LCNSettingsSubcategory = class LCNSettingsSubcategory { static "for" (tab_id, id) { const domid = `lcnssc_${tab_id}_${id}` const inst = cont(document.getElementById(domid), x => x.$LCNSettingsSubcategory) if (inst == null) { const fieldset = document.createElement("fieldset") const legend = document.createElement("legend") fieldset.id = domid fieldset.appendChild(legend) // XXX: extend_tab only takes a string so this hacky workaround is used to let us use the regular dom api Options.extend_tab(tab_id, ``) const div = cont(document.getElementById(`__${domid}`), x => x.parentElement) assert.ok(div) div.replaceChildren(fieldset) return new this(tab_id, id, fieldset) } else { return inst } } constructor (tab_id, id, fieldset) { this._tab_id = tab_id this._id = id this._fieldset = fieldset this._legend = this._fieldset.querySelector("legend") this._fieldset.$LCNSettingsSubcategory = this } "getLabel" () { return this._label; } "setLabel" (label) { this._legend.innerText = this._label = label; return this; } "addSetting" (setting) { assert.ok(setting instanceof LCNSetting) setting.__setIdPrefix(`lcnsetting_${this._tab_id}_${this._id}`) if (!setting.isHidden() && setting.__builtinDOMConstructor != null) { const div = setting.__builtinDOMConstructor() div.classList.add("lcn-setting-entry") this._fieldset.appendChild(div) } return this } } $().ready(() => { LCNSite.INSTANCE = new LCNSite(); for (const clazz of [ LCNPost, LCNPostInfo, LCNThread, LCNPostContainer, LCNPostWrapper ]) { clazz.allNodes = (node=document) => node.querySelectorAll(clazz.selector) clazz.all = (node=document) => Array.prototype.map.apply(clazz.allNodes(node), [ elem => clazz.assign(elem) ]); clazz.clear = (node=document) => Array.prototype.forEach.apply(clazz.allNodes(node), [ elem => elem[clazz.nodeAttrib] = null ]) clazz.forEach = (fn, node=document) => clazz.allNodes(node).forEach(elem => fn(clazz.assign(elem))) clazz.filter = (fn, node=document) => clazz.all(node).filter(fn) clazz.find = fn => clazz.all().find(fn) clazz.first = (node=document) => { return clazz.assign(node.querySelector(clazz.selector)); } clazz.last = (node=document) => clazz.assign(Array.prototype.at.apply(clazz.allNodes(node), [ -1 ])) } // XXX: May be a cleaner way to do this but this should be fine for now. for (const clazz of [ LCNPostContainer, LCNPostWrapper, LCNThread, LCNPost ]) { void clazz.all(); } $(document).on("new_post", (e, post) => { if (LCNSite.INSTANCE.isModRecentsPage()) { void LCNPostWrapper.all() } else { void LCNPostContainer.all() } }) $(window).on("focus", () => LCNSite.INSTANCE.clearUnseen()) $(document.body).on("mousemove", () => LCNSite.INSTANCE.clearUnseen()) })