diff --git a/js/lcn/classes.js b/js/lcn/classes.js new file mode 100644 index 00000000..b33fb26f --- /dev/null +++ b/js/lcn/classes.js @@ -0,0 +1,283 @@ +"use-strict"; +/** + * @file Supporting classes for leftychan javascript. + * @author jonsmy + */ + +globalThis.LCNSite = class LCNSite { + static INSTANCE = new this() + + #isModerator = document.body.classList.contains("is-moderator"); + #isThreadPage = document.body.classList.contains("active-thread"); + #isBoardPage = document.body.classList.contains("active-board"); + #isCatalogPage = document.body.classList.contains("active-catalog"); + + #isModPage = location.pathname == "/mod.php" + #isModRecentsPage = this.#isModPage && (location.search == "?/recent" || location.search.startsWith("?/recent/")) + #isModReportsPage = this.#isModPage && (location.search == "?/reports" || location.search.startsWith("?/reports/")) + #isModLogPage = this.#isModPage && (location.search == "?/log" || location.search.startsWith("?/log/")) + + "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; } +} + +globalThis.LCNPostInfo = class LCNPostInfo { + + #boardId = null; + #threadId = null; + #postId = null; + #name = null; + #email = null; + #capcode = null; + #flag = null; + #ip = null; + #subject = null; + #createdAt = null; + + #parent = null; + #isThread = false; + #isReply = false; + #isLocked = false; + #isSticky = false; + + static "assign" (post) { return post.$LCNPostInfo ?? (post.$LCNPostInfo = this.from(post)); } + 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 = intro.querySelector(".subject")?.innerText ?? null + inst.#name = intro.querySelector(".name")?.innerText ?? null + inst.#email = intro.querySelector(".email")?.href.slice(7) ?? null + inst.#flag = intro.querySelector(".flag")?.src.split("/").reverse()[0].slice(0, -4) ?? null + + inst.#capcode = intro.querySelector(".capcode")?.innerText ?? null + inst.#ip = intro.querySelector(".ip-link")?.innerText ?? null + 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; } + + "isSticky" () { return this.#isSticky; } + "isLocked" () { return this.#isLocked; } + "isThread" () { return this.#isThread; } + "isReply" () { return this.#isReply; } + +} + +globalThis.LCNPost = class LCNPost { + + #parent = null; + #post = null; + #info = null; + #ipLink = null; + #controls = null; + #customControlsSeperatorNode = null; + + static "assign" (post) { return post.$LCNPost ?? (post.$LCNPost = this.from(post)); } + static "from" (post) { return new this(post); } + + "constructor" (post) { + 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 = arrLast(post.querySelectorAll(".controls")) + + 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; } + + static #NBSP = String.fromCharCode(160); + "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.LCNThread = class LCNThread { + + #parent = null; + #thread = null; + #op = null; + + static "assign" (thread) { return thread.$LCNThread ?? (thread.$LCNThread = 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")) + + assert.equal(this.#op.getParent(), null, "Op should not have parent.") + this.#op.__setParent(this) + } + + "getThread" () { return this.#thread; } + "getOP" () { return this.#op; } + "getPosts" () { return Array.prototype.map.apply(this.#thread.querySelectorAll(".post"), [ el => LCNPost.assign(el) ]); } + + "getParent" () { return this.#parent; } + "__setParent" (inst) { return this.#parent = inst; } +} + + +globalThis.LCNPostContainer = class LCNPostContainer { + + #parent = null; + #container = null; + #content = null; + #postId = null; + #boardId = null; + + static "assign" (container) { return container.$LCNPostContainer ?? (container.$LCNPostContainer = 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.#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) + } + + "getContainer" () { return this.#container; } + "getContent" () { return this.#content; } + "getBoardId" () { return this.#boardId; } + "getPostId" () { return this.#postId; } + + "getParent" () { return this.#parent; } + "__setParent" (inst) { return this.#parent = inst; } + +} + +globalThis.LCNPostWrapper = class LCNPostWrapper { + + #wrapper = null; + #eitaLink = null; + #eitaId = null; + #eitaHref = null + #content = null; + + static "assign" (wrapper) { return wrapper.$LCNPostWrapper ?? (wrapper.$LCNPostWrapper = this.from(wrapper)); } + static "from" (wrapper) { return new this(wrapper); } + + "constructor" (wrapper) { + 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) + } + + "getWrapper" () { return this.#wrapper } + "getContent" () { return this.#content; } + "getEitaId" () { return this.#eitaId; } + "getEitaHref" () { return this.#eitaHref; } + "getEitaLink" () { return this.#eitaLink; } + +} + +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) ]) + +$().ready(() => { + 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) + clazz.find = fn => clazz.all().find(fn) + } + + // XXX: May be a cleaner way to do this but this should be fine for now. + for (const clazz of clazzes) { void clazz.all(); } + $(document).on("new_post", (e, post) => { + if (LCNSite.INSTANCE.isRecentsPage()) { + void LCNPostWrapper.all() + } else { + void LCNPostContainer.all() + } + }) +}) diff --git a/js/lcn/utils.js b/js/lcn/utils.js new file mode 100644 index 00000000..28f97e79 --- /dev/null +++ b/js/lcn/utils.js @@ -0,0 +1,34 @@ +"use-strict"; +/** + * @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) { + const err = new Error(`Assertion Failed. ${message}`) + err.data = { actual, expected} + Error.captureStackTrace(err, assert.equal) + debugger + throw err + } + }, + "ok": (actual, message="No message set") => { + if (!actual) { + const err = new Error(`Assertion Failed. ${message}`) + err.data = { actual } + Error.captureStackTrace(err, assert.ok) + debugger + throw err + } + } +}