diff --git a/js/lcn/classes.js b/js/lcn/classes.js index 035173e9..18fcf436 100644 --- a/js/lcn/classes.js +++ b/js/lcn/classes.js @@ -6,6 +6,34 @@ globalThis.LCNSite = class LCNSite { static INSTANCE = null; + 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 + } + #isModerator = document.body.classList.contains("is-moderator"); #isThreadPage = document.body.classList.contains("active-thread"); #isBoardPage = document.body.classList.contains("active-board"); @@ -53,10 +81,28 @@ globalThis.LCNSite = class LCNSite { 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"); } + + #generatedStyle = null + "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.LCNPostInfo = class LCNPostInfo { + static nodeAttrib = "$LCNPostInfo"; + static selector = ".post:not(.grid-li)"; #boardId = null; #threadId = null; #postId = null; @@ -74,7 +120,7 @@ globalThis.LCNPostInfo = class LCNPostInfo { #isLocked = false; #isSticky = false; - static "assign" (post) { return post.$LCNPostInfo ?? (post.$LCNPostInfo = this.from(post)); } + static "assign" (post) { return post[this.nodeAttrib] ?? (post[this.nodeAttrib] = this.from(post)); } static "from" (post) { assert.ok(post.classList.contains("post"), "Arty must be expected Element.") const inst = new this() @@ -130,6 +176,8 @@ globalThis.LCNPostInfo = class LCNPostInfo { globalThis.LCNPost = class LCNPost { + static nodeAttrib = "$LCNPost"; + static selector = ".post:not(.grid-li)"; #parent = null; #post = null; #info = null; @@ -137,7 +185,7 @@ globalThis.LCNPost = class LCNPost { #controls = null; #customControlsSeperatorNode = null; - static "assign" (post) { return post.$LCNPost ?? (post.$LCNPost = this.from(post)); } + static "assign" (post) { return post[this.nodeAttrib] ?? (post[this.nodeAttrib] = this.from(post)); } static "from" (post) { return new this(post); } "constructor" (post) { @@ -146,7 +194,7 @@ globalThis.LCNPost = class LCNPost { this.#post = post this.#info = LCNPostInfo.assign(post) this.#ipLink = intro.querySelector(".ip-link") - this.#controls = arrLast(post.querySelectorAll(".controls")) + 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) @@ -193,26 +241,28 @@ globalThis.LCNPost = class LCNPost { globalThis.LCNThread = class LCNThread { + static nodeAttrib = "$LCNThread"; + static selector = ".thread:not(.grid-li)"; + #element = null; #parent = null; - #thread = null; #op = null; - static "assign" (thread) { return thread.$LCNThread ?? (thread.$LCNThread = this.from(thread)); } + static "assign" (thread) { return thread[this.nodeAttrib] ?? (thread[this.nodeAttrib] = this.from(thread)); } static "from" (thread) { return new this(thread); } "constructor" (thread) { assert.ok(thread.classList.contains("thread"), "Arty must be expected Element.") - this.#thread = thread - this.#op = LCNPost.assign(this.#thread.querySelector(".post.op")) + 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.#thread; } + "getElement" () { return this.#element; } "getContent" () { return this.#op; } - "getPosts" () { return Array.prototype.map.apply(this.#thread.querySelectorAll(".post"), [ el => LCNPost.assign(el) ]); } - "getReplies" () { return Array.prototype.map.apply(this.#thread.querySelectorAll(".post:not(.op)"), [ el => LCNPost.assign(el) ]); } + "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; } @@ -221,19 +271,21 @@ globalThis.LCNThread = class LCNThread { globalThis.LCNPostContainer = class LCNPostContainer { + static nodeAttrib = "$LCNPostContainer"; + static selector = ".postcontainer"; #parent = null; - #container = null; + #element = null; #content = null; #postId = null; #boardId = null; - static "assign" (container) { return container.$LCNPostContainer ?? (container.$LCNPostContainer = this.from(container)); } + static "assign" (container) { return container[this.nodeAttrib] ?? (container[this.nodeAttrib] = this.from(container)); } static "from" (container) { return new this(container); } "constructor" (container) { assert.ok(container.classList.contains("postcontainer"), "Arty must be expected Element.") const child = container.querySelector(".thread, .post") - this.#container = container + 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) @@ -242,7 +294,7 @@ globalThis.LCNPostContainer = class LCNPostContainer { this.#content.__setParent(this) } - "getContainer" () { return this.#container; } + "getElement" () { return this.#element; } "getContent" () { return this.#content; } "getBoardId" () { return this.#boardId; } "getPostId" () { return this.#postId; } @@ -254,13 +306,15 @@ globalThis.LCNPostContainer = class LCNPostContainer { globalThis.LCNPostWrapper = class LCNPostWrapper { + static nodeAttrib = "$LCNPostWrapper"; + static selector = ".post-wrapper"; #wrapper = null; #eitaLink = null; #eitaId = null; #eitaHref = null #content = null; - static "assign" (wrapper) { return wrapper.$LCNPostWrapper ?? (wrapper.$LCNPostWrapper = this.from(wrapper)); } + static "assign" (wrapper) { return wrapper[this.nodeAttrib] ?? (wrapper[this.nodeAttrib] = this.from(wrapper)); } static "from" (wrapper) { return new this(wrapper); } "constructor" (wrapper) { @@ -298,23 +352,140 @@ globalThis.LCNPostWrapper = class LCNPostWrapper { } -globalThis.LCNPost.all = () => Array.prototype.map.apply(document.querySelectorAll(".post:not(.grid-li)"), [ node => LCNPost.assign(node) ]); -globalThis.LCNThread.all = () => Array.prototype.map.apply(document.querySelectorAll(".thread:not(.grid-li)"), [ node => LCNThread.assign(node) ]); -globalThis.LCNPostContainer.all = () => Array.prototype.map.apply(document.querySelectorAll(".postcontainer"), [ node => LCNPostContainer.assign(node) ]); -globalThis.LCNPostWrapper.all = () => Array.prototype.map.apply(document.querySelectorAll(".post-wrapper"), [ node => LCNPostWrapper.assign(node) ]); +globalThis.LCNSetting = class LCNSetting { + #id = null; + #eventId = null; + #label = null; + #value = null; + #valueDefault = null; + + static "build" (id) { return new this(id); } + + "constructor" (id) { + 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 + } + } + + "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}`; } +} + +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 txt = document.createElement("label") + txt.innerText = this.getLabel() + 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(txt) + return div + } +} + +globalThis.LCNSettingsSubcategory = class LCNSettingsSubcategory { + + #tab_id = null; + #id = null; + + #fieldset = null; + #legend = null + #label = null; + + static "for" (tab_id, id) { + const domid = `lcnssc_${tab_id}_${id}` + const inst = document.getElementById(domid)?.$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 = document.getElementById(`__${domid}`)?.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.__builtinDOMConstructor != null) { + const div = setting.__builtinDOMConstructor() + div.classList.add("lcn-setting-entry") + this.#fieldset.appendChild(div) + } + + return this + } + +} $().ready(() => { LCNSite.INSTANCE = new LCNSite(); - const clazzes = [ LCNPost, LCNThread, LCNPostContainer, LCNPostWrapper ] - for (const clazz of clazzes) { - clazz.forEach = fn => clazz.all().forEach(fn) - clazz.filter = fn => clazz.all().filter(fn) + 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) => 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 clazzes) { void clazz.all(); } + for (const clazz of [ LCNPostContainer, LCNPostWrapper, LCNThread, LCNPost ]) { void clazz.all(); } $(document).on("new_post", (e, post) => { if (LCNSite.INSTANCE.isModRecentsPage()) { void LCNPostWrapper.all() @@ -322,4 +493,7 @@ $().ready(() => { void LCNPostContainer.all() } }) + + $(window).on("focus", () => LCNSite.INSTANCE.clearUnseen()) + $(document.body).on("mousemove", () => LCNSite.INSTANCE.clearUnseen()) }) diff --git a/js/lcn/utils.js b/js/lcn/utils.js index 907fce95..2a5cb3bd 100644 --- a/js/lcn/utils.js +++ b/js/lcn/utils.js @@ -1,16 +1,8 @@ -/** + /** * @file Utils for leftychan javascript. * @author jonsmy */ -const arrLast = arr => arr[arr.length-1] ?? undefined; -const getConfigBool = (k,d) => { const v = localStorage.getItem(`jon-modjs::${k}`); return v ? v == "1" : d; } -const writeCSSStyle = textContent => { - const style = document.createElement("style") - style.textContent = textContent - document.head.appendChild(style) -} - const assert = { "equal": (actual, expected, message="No message set") => { if (actual !== expected) {