mirror of
https://github.com/LalleSX/4chan-XZ.git
synced 2026-01-30 09:48:12 +01:00
1320 lines
43 KiB
TypeScript
1320 lines
43 KiB
TypeScript
/*
|
|
* decaffeinate suggestions:
|
|
* DS101: Remove unnecessary use of Array.from
|
|
* DS102: Remove unnecessary code created because of implicit returns
|
|
* DS205: Consider reworking code to avoid use of IIFEs
|
|
* DS207: Consider shorter variations of null checks
|
|
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
|
|
*/
|
|
import Callbacks from '../classes/Callbacks'
|
|
import CatalogThread from '../classes/CatalogThread'
|
|
import Notice from '../classes/Notice'
|
|
import Post from '../classes/Post'
|
|
import Thread from '../classes/Thread'
|
|
import Config from '../config/Config'
|
|
import Filter from '../Filtering/Filter'
|
|
import PostHiding from '../Filtering/PostHiding'
|
|
import ThreadHiding from '../Filtering/ThreadHiding'
|
|
import { Board, c, Conf, d, doc, g } from '../globals/globals'
|
|
import Main from '../main/Main'
|
|
import Menu from '../Menu/Menu'
|
|
import CatalogLinks from '../Miscellaneous/CatalogLinks'
|
|
import RelativeDates from '../Miscellaneous/RelativeDates'
|
|
import ThreadWatcher from '../Monitoring/ThreadWatcher'
|
|
import $ from '../platform/$'
|
|
import $$ from '../platform/$$'
|
|
import { dict, SECOND } from '../platform/helpers'
|
|
import QuotePreview from '../Quotelinks/QuotePreview'
|
|
import BoardConfig from './BoardConfig'
|
|
import Get from './Get'
|
|
import Header from './Header'
|
|
import NavLinksPage from './Index/NavLinks.html'
|
|
import PageList from './Index/PageList.html'
|
|
import UI from './UI'
|
|
interface Index {
|
|
enabled: any
|
|
menu: any
|
|
req: boolean
|
|
liveThreadData: any
|
|
pageNum: any
|
|
currentPage: any
|
|
pagesNum: number
|
|
changed: any
|
|
showHiddenThreads: boolean
|
|
threadPosition: any
|
|
threadsNumPerPage: any
|
|
selectSort: any
|
|
initFinishedFired: boolean
|
|
cb: any
|
|
loaded: any
|
|
lastLongThresholds: any
|
|
inputs: any
|
|
selectRev: any
|
|
hashCommands: any
|
|
search: any
|
|
liveThreadDict: any
|
|
parsedThreads: any
|
|
replyData: any
|
|
selectMode: any
|
|
lastLongOptions: any
|
|
sortedThreadIDs: any
|
|
hideLabel: any
|
|
liveThreadIDs: any
|
|
notice: any
|
|
nTimeout: any
|
|
searchInput: any
|
|
update(): unknown
|
|
currentSort(currentSort: any): any
|
|
root(board: any, root: any): unknown
|
|
navLinks(topNavPos: Element, navLinks: any): unknown
|
|
pagelist(pagelist: any, pagelist1: any): unknown
|
|
endNotice(): unknown
|
|
threadsOnPage(pageNum: number): unknown
|
|
buildStructure(threadIDs: any): unknown
|
|
enabledOn(BOARD: Board): unknown
|
|
toggleHide(thread: any): unknown
|
|
sort(): unknown
|
|
buildIndex(): unknown
|
|
pushState(arg0: { mode: any }): unknown
|
|
pageLoad(arg0: boolean): unknown
|
|
saveSort(): unknown
|
|
saveLastLongThresholds(i: number): unknown
|
|
getCurrentPage(): unknown
|
|
setState(arg0: { search: any; mode: any; sort: any; page: any; hash: string }): unknown
|
|
processHash(): unknown
|
|
userPageNav(arg0: number): unknown
|
|
buildCatalogReplies(arg0: any): unknown
|
|
savePerBoard(arg0: string, currentSort: any): unknown
|
|
buildPagelist(): unknown
|
|
setupSearch(): unknown
|
|
setupMode(): unknown
|
|
setupSort(): unknown
|
|
setPage(): unknown
|
|
scrollToIndex(): unknown
|
|
getPagesNum(): number
|
|
getMaxPageNum(): unknown
|
|
isHidden(threadID: any): unknown
|
|
load(arg0: string, arg1: string, load: any): any
|
|
button(button: any, arg1: string): unknown
|
|
parse(response: any): unknown
|
|
parseThreadList(pages: any): unknown
|
|
buildReplies(threads: any[]): any
|
|
updateHideLabel(): unknown
|
|
isHiddenReply(ID: any, data: any): unknown
|
|
querySearch(search: any): any
|
|
sortOnTop(arg0: (obj: any) => any): unknown
|
|
buildCatalog(threadIDs: any): unknown
|
|
buildThreads(threadIDs: any, arg1: boolean, arg2: any): unknown
|
|
buildCatalogPart(arg0: any): unknown
|
|
buildCatalogViews(threads: any): unknown
|
|
sizeCatalogViews(threads: any): unknown
|
|
onSearchInput(): unknown
|
|
searchMatch(arg0: any, keywords: any): unknown
|
|
}
|
|
const Index: Index = {
|
|
showHiddenThreads: false,
|
|
changed: {},
|
|
|
|
enabledOn({ siteID, boardID }) {
|
|
return Conf['JSON Index'] && (g.sites[siteID].software === 'yotsuba') && (boardID !== 'f')
|
|
},
|
|
|
|
init() {
|
|
let input, inputs, name
|
|
if (g.VIEW !== 'index') { return }
|
|
|
|
// For IndexRefresh events
|
|
$.one(d, '4chanXInitFinished', this.cb.initFinished)
|
|
$.on(d, 'PostsInserted', this.cb.postsInserted)
|
|
|
|
if (!this.enabledOn(g.BOARD)) { return }
|
|
|
|
this.enabled = true
|
|
|
|
Callbacks.Post.push({
|
|
name: 'Index Page Numbers',
|
|
cb: this.node
|
|
})
|
|
Callbacks.CatalogThread.push({
|
|
name: 'Catalog Features',
|
|
cb: this.catalogNode
|
|
})
|
|
|
|
this.search = history.state?.searched || ''
|
|
if (history.state?.mode) {
|
|
Conf['Index Mode'] = history.state?.mode
|
|
}
|
|
this.currentSort = history.state?.sort
|
|
if (!this.currentSort) {
|
|
this.currentSort = typeof Conf['Index Sort'] === 'object' ? (
|
|
Conf['Index Sort'][g.BOARD.ID] || 'bump'
|
|
) : (
|
|
Conf['Index Sort']
|
|
)
|
|
}
|
|
this.currentPage = this.getCurrentPage()
|
|
this.processHash()
|
|
|
|
$.addClass(doc, 'index-loading', `${Conf['Index Mode'].replace(/\ /g, '-')}-mode`)
|
|
$.on(window, 'popstate', this.cb.popstate)
|
|
$.on(d, 'scroll', this.scroll)
|
|
$.on(d, 'SortIndex', this.cb.resort)
|
|
|
|
// Header refresh button
|
|
this.button = $.el('a', {
|
|
className: 'fa fa-refresh',
|
|
title: 'Refresh',
|
|
href: 'javascript:;',
|
|
textContent: 'Refresh Index'
|
|
}
|
|
)
|
|
$.on(this.button, 'click', () => Index.update())
|
|
Header.addShortcut('index-refresh', this.button, 590)
|
|
|
|
// Header "Index Navigation" submenu
|
|
const entries = []
|
|
this.inputs = (inputs = dict())
|
|
for (name in Config.Index) {
|
|
const arr = Config.Index[name]
|
|
if (arr instanceof Array) {
|
|
const label = UI.checkbox(name, `${name[0]}${name.slice(1).toLowerCase()}`)
|
|
label.title = arr[1]
|
|
entries.push({ el: label })
|
|
input = label.firstChild
|
|
$.on(input, 'change', $.cb.checked)
|
|
inputs[name] = input
|
|
}
|
|
}
|
|
$.on(inputs['Show Replies'], 'change', this.cb.replies)
|
|
$.on(inputs['Catalog Hover Expand'], 'change', this.cb.hover)
|
|
$.on(inputs['Pin Watched Threads'], 'change', this.cb.resort)
|
|
$.on(inputs['Anchor Hidden Threads'], 'change', this.cb.resort)
|
|
|
|
const watchSettings = function (e) {
|
|
if (input = $.getOwn(inputs, e.target.name)) {
|
|
input.checked = e.target.checked
|
|
return $.event('change', null, input)
|
|
}
|
|
}
|
|
$.on(d, 'OpenSettings', () => $.on($.id('fourchanx-settings'), 'change', watchSettings))
|
|
|
|
const sortEntry = UI.checkbox('Per-Board Sort Type', 'Per-board sort type', (typeof Conf['Index Sort'] === 'object'))
|
|
sortEntry.title = 'Set the sorting order of each board independently.'
|
|
$.on(sortEntry.firstChild, 'change', this.cb.perBoardSort)
|
|
entries.splice(3, 0, { el: sortEntry })
|
|
|
|
Header.menu.addEntry({
|
|
el: $.el('span',
|
|
{ textContent: 'Index Navigation' }),
|
|
order: 100,
|
|
subEntries: entries
|
|
})
|
|
|
|
// Navigation links at top of index
|
|
this.navLinks = $.el('div', { className: 'navLinks json-index' })
|
|
$.extend(this.navLinks, { innerHTML: NavLinksPage })
|
|
$('.cataloglink a', this.navLinks).href = CatalogLinks.catalog()
|
|
if (!BoardConfig.isArchived(g.BOARD.ID)) { $('.archlistlink', this.navLinks).hidden = true }
|
|
$.on($('#index-last-refresh a', this.navLinks), 'click', this.cb.refreshFront)
|
|
|
|
// Search field
|
|
this.searchInput = $('#index-search', this.navLinks)
|
|
this.setupSearch()
|
|
$.on(this.searchInput, 'input', this.onSearchInput)
|
|
$.on($('#index-search-clear', this.navLinks), 'click', this.clearSearch)
|
|
|
|
// Hidden threads toggle
|
|
this.hideLabel = $('#hidden-label', this.navLinks)
|
|
$.on($('#hidden-toggle a', this.navLinks), 'click', this.cb.toggleHiddenThreads)
|
|
|
|
// Drop-down menus and reverse sort toggle
|
|
this.selectRev = $('#index-rev', this.navLinks)
|
|
this.selectMode = $('#index-mode', this.navLinks)
|
|
this.selectSort = $('#index-sort', this.navLinks)
|
|
this.selectSize = $('#index-size', this.navLinks)
|
|
$.on(this.selectRev, 'change', this.cb.sort)
|
|
$.on(this.selectMode, 'change', this.cb.mode)
|
|
$.on(this.selectSort, 'change', this.cb.sort)
|
|
$.on(this.selectSize, 'change', $.cb.value)
|
|
$.on(this.selectSize, 'change', this.cb.size)
|
|
for (const select of [this.selectMode, this.selectSize]) {
|
|
select.value = Conf[select.name]
|
|
}
|
|
this.selectRev.checked = /-rev$/.test(Index.currentSort)
|
|
this.selectSort.value = Index.currentSort.replace(/-rev$/, '')
|
|
|
|
// Last Long Reply options
|
|
this.lastLongOptions = $('#lastlong-options', this.navLinks)
|
|
this.lastLongInputs = $$('input', this.lastLongOptions)
|
|
this.lastLongThresholds = [0, 0]
|
|
this.lastLongOptions.hidden = (this.selectSort.value !== 'lastlong')
|
|
for (let i = 0; i < this.lastLongInputs.length; i++) {
|
|
input = this.lastLongInputs[i]
|
|
$.on(input, 'change', this.cb.lastLongThresholds)
|
|
const tRaw = Conf[`Last Long Reply Thresholds ${i}`]
|
|
input.value = (this.lastLongThresholds[i] =
|
|
typeof tRaw === 'object' ? (tRaw[g.BOARD.ID] ?? 100) : tRaw)
|
|
}
|
|
|
|
// Thread container
|
|
this.root = $.el('div', { className: 'board json-index' })
|
|
$.on(this.root, 'click', this.cb.hoverToggle)
|
|
this.cb.size()
|
|
this.cb.hover()
|
|
|
|
// Page list
|
|
this.pagelist = $.el('div', { className: 'pagelist json-index' })
|
|
$.extend(this.pagelist, { innerHTML: PageList })
|
|
$('.cataloglink a', this.pagelist).href = CatalogLinks.catalog()
|
|
$.on(this.pagelist, 'click', this.cb.pageNav)
|
|
|
|
this.update(true)
|
|
|
|
$.onExists(doc, 'title + *', () => d.title = d.title.replace(/\ -\ Page\ \d+/, ''))
|
|
|
|
$.onExists(doc, '.board > .thread > .postContainer, .board + *', function () {
|
|
let el
|
|
g.SITE.Build.hat = $('.board > .thread > img:first-child')
|
|
if (g.SITE.Build.hat) {
|
|
g.BOARD.threads.forEach(function (thread) {
|
|
if (thread.nodes.root) {
|
|
return $.prepend(thread.nodes.root, g.SITE.Build.hat.cloneNode(false))
|
|
}
|
|
})
|
|
$.addClass(doc, 'hats-enabled')
|
|
$.addStyle(`.catalog-thread::after {background-image: url(${g.SITE.Build.hat.src});}`)
|
|
}
|
|
|
|
const board = $('.board')
|
|
$.replace(board, Index.root)
|
|
if (Index.loaded) {
|
|
$.event('PostsInserted', null, Index.root)
|
|
}
|
|
// Hacks:
|
|
// - When removing an element from the document during page load,
|
|
// its ancestors will still be correctly created inside of it.
|
|
// - Creating loadable elements inside of an origin-less document
|
|
// will not download them.
|
|
// - Combine the two and you get a download canceller!
|
|
// Does not work on Firefox unfortunately. bugzil.la/939713
|
|
try {
|
|
d.implementation.createDocument(null, null, null).appendChild(board)
|
|
} catch (error) { }
|
|
|
|
for (el of $$('.navLinks')) { $.rm(el) }
|
|
$.rm($.id('ctrl-top'))
|
|
const topNavPos = $.id('delform').previousElementSibling
|
|
$.before(topNavPos, $.el('hr'))
|
|
$.before(topNavPos, Index.navLinks)
|
|
const timeEl = $('#index-last-refresh time', Index.navLinks)
|
|
if (timeEl.dataset.utc) { return RelativeDates.update(timeEl) }
|
|
})
|
|
|
|
return Main.ready(function () {
|
|
let pagelist
|
|
if (pagelist = $('.pagelist')) {
|
|
$.replace(pagelist, Index.pagelist)
|
|
}
|
|
return $.rmClass(doc, 'index-loading')
|
|
})
|
|
},
|
|
|
|
scroll() {
|
|
if (Index.req || !Index.liveThreadData || (Conf['Index Mode'] !== 'infinite') || (window.scrollY <= (doc.scrollHeight - (300 + window.innerHeight)))) { return }
|
|
if (Index.pageNum == null) { Index.pageNum = Index.currentPage } // Avoid having to pushState to keep track of the current page
|
|
|
|
const pageNum = ++Index.pageNum
|
|
if (pageNum > Index.pagesNum) { return Index.endNotice() }
|
|
|
|
const threadIDs = Index.threadsOnPage(pageNum)
|
|
return Index.buildStructure(threadIDs)
|
|
},
|
|
|
|
endNotice: (function () {
|
|
let notify = false
|
|
const reset = () => notify = false
|
|
return function () {
|
|
if (notify) { return }
|
|
notify = true
|
|
new Notice('info', "Last page reached.", 2)
|
|
return setTimeout(reset, 3 * SECOND)
|
|
}
|
|
})(),
|
|
|
|
menu: {
|
|
init() {
|
|
if ((g.VIEW !== 'index') || !Conf['Menu'] || !Conf['Thread Hiding Link'] || !Index.enabledOn(g.BOARD)) { return }
|
|
|
|
return Menu.menu.addEntry({
|
|
el: $.el('a', {
|
|
href: 'javascript:;',
|
|
className: 'has-shortcut-text'
|
|
}
|
|
, { innerHTML: "<span></span><span class=\"shortcut-text\">Shift+click</span>" }),
|
|
order: 20,
|
|
open({ thread }) {
|
|
if (Conf['Index Mode'] !== 'catalog') { return false }
|
|
this.el.firstElementChild.textContent = thread.isHidden ?
|
|
'Unhide'
|
|
:
|
|
'Hide'
|
|
if (this.cb) { $.off(this.el, 'click', this.cb) }
|
|
this.cb = function () {
|
|
$.event('CloseMenu')
|
|
return Index.toggleHide(thread)
|
|
}
|
|
$.on(this.el, 'click', this.cb)
|
|
return true
|
|
}
|
|
})
|
|
}
|
|
},
|
|
|
|
node() {
|
|
if (this.isReply || this.isClone || (Index.threadPosition[this.ID] == null)) { return }
|
|
return this.thread.setPage(Math.floor(Index.threadPosition[this.ID] / Index.threadsNumPerPage) + 1)
|
|
},
|
|
|
|
catalogNode() {
|
|
return $.on(this.nodes.root, 'mousedown click', e => {
|
|
if ((e.button !== 0) || !e.shiftKey) { return }
|
|
if (e.type === 'click') { Index.toggleHide(this.thread) }
|
|
return e.preventDefault()
|
|
})
|
|
}, // Also on mousedown to prevent highlighting text.
|
|
|
|
toggleHide(thread) {
|
|
if (Index.showHiddenThreads) {
|
|
ThreadHiding.show(thread)
|
|
if (!ThreadHiding.db.get({ boardID: thread.board.ID, threadID: thread.ID })) { return }
|
|
// Don't save when un-hiding filtered threads.
|
|
} else {
|
|
ThreadHiding.hide(thread)
|
|
}
|
|
return ThreadHiding.saveHiddenState(thread)
|
|
},
|
|
|
|
cycleSortType() {
|
|
let i
|
|
const types = [...Array.from(Index.selectSort.options)].filter(option => !option.disabled)
|
|
for (i = 0; i < types.length; i++) {
|
|
const type = types[i]
|
|
if (type.selected) { break }
|
|
}
|
|
types[(i + 1) % types.length].selected = true
|
|
return $.event('change', null, Index.selectSort)
|
|
},
|
|
|
|
cb: {
|
|
initFinished() {
|
|
Index.initFinishedFired = true
|
|
return $.queueTask(() => Index.cb.postsInserted())
|
|
},
|
|
|
|
postsInserted() {
|
|
if (!Index.initFinishedFired) { return }
|
|
let n = 0
|
|
g.posts.forEach(function (post) {
|
|
if (!post.isFetchedQuote && !post.indexRefreshSeen && doc.contains(post.nodes.root)) {
|
|
post.indexRefreshSeen = true
|
|
return n++
|
|
}
|
|
})
|
|
if (n) { return $.event('IndexRefresh') }
|
|
},
|
|
|
|
toggleHiddenThreads() {
|
|
$('#hidden-toggle a', Index.navLinks).textContent = (Index.showHiddenThreads = !Index.showHiddenThreads) ?
|
|
'Hide'
|
|
:
|
|
'Show'
|
|
Index.sort()
|
|
return Index.buildIndex()
|
|
},
|
|
|
|
mode() {
|
|
Index.pushState({ mode: this.value })
|
|
return Index.pageLoad(false)
|
|
},
|
|
|
|
sort() {
|
|
const value = Index.selectRev.checked ? Index.selectSort.value + "-rev" : Index.selectSort.value
|
|
Index.pushState({ sort: value })
|
|
return Index.pageLoad(false)
|
|
},
|
|
|
|
resort(e) {
|
|
Index.changed.order = true
|
|
if (!e?.detail?.deferred) { return Index.pageLoad(false) }
|
|
},
|
|
|
|
perBoardSort() {
|
|
Conf['Index Sort'] = this.checked ? dict() : ''
|
|
Index.saveSort()
|
|
for (let i = 0; i < 2; i++) {
|
|
Conf[`Last Long Reply Thresholds ${i}`] = this.checked ? dict() : ''
|
|
Index.saveLastLongThresholds(i)
|
|
}
|
|
},
|
|
|
|
lastLongThresholds() {
|
|
const i = [...Array.from(this.parentNode.children)].indexOf(this)
|
|
const value = +this.value
|
|
if (!Number.isFinite(value)) {
|
|
this.value = Index.lastLongThresholds[i]
|
|
return
|
|
}
|
|
Index.lastLongThresholds[i] = value
|
|
Index.saveLastLongThresholds(i)
|
|
Index.changed.order = true
|
|
return Index.pageLoad(false)
|
|
},
|
|
|
|
size(e) {
|
|
if (Conf['Index Mode'] !== 'catalog') {
|
|
$.rmClass(Index.root, 'catalog-small')
|
|
$.rmClass(Index.root, 'catalog-large')
|
|
} else if (Conf['Index Size'] === 'small') {
|
|
$.addClass(Index.root, 'catalog-small')
|
|
$.rmClass(Index.root, 'catalog-large')
|
|
} else {
|
|
$.addClass(Index.root, 'catalog-large')
|
|
$.rmClass(Index.root, 'catalog-small')
|
|
}
|
|
if (e) { return Index.buildIndex() }
|
|
},
|
|
|
|
replies() {
|
|
return Index.buildIndex()
|
|
},
|
|
|
|
hover() {
|
|
return doc.classList.toggle('catalog-hover-expand', Conf['Catalog Hover Expand'])
|
|
},
|
|
|
|
hoverToggle(e) {
|
|
if (Conf['Catalog Hover Toggle'] && $.hasClass(doc, 'catalog-mode') && !$.modifiedClick(e) && !$.x('ancestor-or-self::a', e.target)) {
|
|
let thread
|
|
const input = Index.inputs['Catalog Hover Expand']
|
|
input.checked = !input.checked
|
|
$.event('change', null, input)
|
|
if (thread = Get.threadFromNode(e.target)) {
|
|
Index.cb.catalogReplies.call(thread)
|
|
return Index.cb.hoverAdjust.call(thread.OP.nodes)
|
|
}
|
|
}
|
|
},
|
|
|
|
popstate(e) {
|
|
if (e?.state) {
|
|
const { searched, mode, sort } = e.state
|
|
const page = Index.getCurrentPage()
|
|
Index.setState({ search: searched, mode, sort, page, hash: location.hash })
|
|
return Index.pageLoad(false)
|
|
} else {
|
|
// page load or hash change
|
|
const nCommands = Index.processHash()
|
|
if (Conf['Refreshed Navigation'] && nCommands) {
|
|
return Index.update()
|
|
} else {
|
|
return Index.pageLoad()
|
|
}
|
|
}
|
|
},
|
|
|
|
pageNav(e) {
|
|
let a
|
|
if ($.modifiedClick(e)) { return }
|
|
switch (e.target.nodeName) {
|
|
case 'BUTTON':
|
|
e.target.blur()
|
|
a = e.target.parentNode
|
|
break
|
|
case 'A':
|
|
a = e.target
|
|
break
|
|
default:
|
|
return
|
|
}
|
|
if (a.textContent === 'Catalog') { return }
|
|
e.preventDefault()
|
|
return Index.userPageNav(+a.pathname.split(/\/+/)[2] || 1)
|
|
},
|
|
|
|
refreshFront() {
|
|
Index.pushState({ page: 1 })
|
|
return Index.update()
|
|
},
|
|
|
|
catalogReplies() {
|
|
if (Conf['Show Replies'] && $.hasClass(doc, 'catalog-hover-expand') && !this.catalogView.nodes.replies) {
|
|
return Index.buildCatalogReplies(this)
|
|
}
|
|
},
|
|
|
|
hoverAdjust() {
|
|
// Prevent hovered catalog threads from going offscreen.
|
|
let x
|
|
if (!$.hasClass(doc, 'catalog-hover-expand')) { return }
|
|
const rect = this.post.getBoundingClientRect()
|
|
if (x = $.minmax(0, -rect.left, doc.clientWidth - rect.right)) {
|
|
const { style } = this.post
|
|
style.left = `${x}px`
|
|
style.right = `${-x}px`
|
|
return $.one(this.root, 'mouseleave', () => style.left = (style.right = null))
|
|
}
|
|
}
|
|
},
|
|
|
|
scrollToIndex() {
|
|
// Scroll to navlinks, or top of board if navlinks are hidden.
|
|
return Header.scrollToIfNeeded((Index.navLinks.getBoundingClientRect().height ? Index.navLinks : Index.root))
|
|
},
|
|
|
|
getCurrentPage() {
|
|
return +window.location.pathname.split(/\/+/)[2] || 1
|
|
},
|
|
|
|
userPageNav(page) {
|
|
Index.pushState({ page })
|
|
if (Conf['Refreshed Navigation']) {
|
|
return Index.update()
|
|
} else {
|
|
return Index.pageLoad()
|
|
}
|
|
},
|
|
|
|
hashCommands: {
|
|
mode: {
|
|
'paged': 'paged',
|
|
'infinite-scrolling': 'infinite',
|
|
'infinite': 'infinite',
|
|
'all-threads': 'all pages',
|
|
'all-pages': 'all pages',
|
|
'catalog': 'catalog'
|
|
},
|
|
sort: {
|
|
'bump-order': 'bump',
|
|
'last-reply': 'lastreply',
|
|
'last-long-reply': 'lastlong',
|
|
'creation-date': 'birth',
|
|
'reply-count': 'replycount',
|
|
'file-count': 'filecount',
|
|
'posts-per-minute': 'activity'
|
|
}
|
|
},
|
|
|
|
processHash() {
|
|
// XXX https://bugzilla.mozilla.org/show_bug.cgi?id=483304
|
|
let hash = location.href.match(/#.*/)?.[0] || ''
|
|
const state =
|
|
{ replace: true }
|
|
const commands = hash.slice(1).split('/')
|
|
const leftover = []
|
|
for (const command of commands) {
|
|
var mode, sort
|
|
if (mode = $.getOwn(Index.hashCommands.mode, command)) {
|
|
state.mode = mode
|
|
} else if (command === 'index') {
|
|
state.mode = Conf['Previous Index Mode']
|
|
state.page = 1
|
|
} else if (sort = $.getOwn(Index.hashCommands.sort, command.replace(/-rev$/, ''))) {
|
|
state.sort = sort
|
|
if (/-rev$/.test(command)) { state.sort += '-rev' }
|
|
} else if (/^s=/.test(command)) {
|
|
state.search = decodeURIComponent(command.slice(2)).replace(/\+/g, ' ').trim()
|
|
} else {
|
|
leftover.push(command)
|
|
}
|
|
}
|
|
hash = leftover.join('/')
|
|
if (hash) { state.hash = `#${hash}` }
|
|
Index.pushState(state)
|
|
return commands.length - leftover.length
|
|
},
|
|
|
|
pushState(state) {
|
|
let { search, hash, replace } = state
|
|
let pageBeforeSearch = history.state?.oldpage
|
|
if ((search != null) && (search !== Index.search)) {
|
|
state.page = search ? 1 : (pageBeforeSearch || 1)
|
|
if (!search) {
|
|
pageBeforeSearch = undefined
|
|
} else if (!Index.search) {
|
|
pageBeforeSearch = Index.currentPage
|
|
}
|
|
}
|
|
Index.setState(state)
|
|
const pathname = Index.currentPage === 1 ? `/${g.BOARD}/` : `/${g.BOARD}/${Index.currentPage}`
|
|
if (!hash) { hash = '' }
|
|
return history[replace ? 'replaceState' : 'pushState']({
|
|
mode: Conf['Index Mode'],
|
|
sort: Index.currentSort,
|
|
searched: Index.search,
|
|
oldpage: pageBeforeSearch
|
|
}
|
|
, '', `${location.protocol}//${location.host}${pathname}${hash}`)
|
|
},
|
|
|
|
setState({ search, mode, sort, page, hash }) {
|
|
if ((search != null) && (search !== Index.search)) {
|
|
Index.changed.search = true
|
|
Index.search = search
|
|
}
|
|
if ((mode != null) && (mode !== Conf['Index Mode'])) {
|
|
Index.changed.mode = true
|
|
Conf['Index Mode'] = mode
|
|
$.set('Index Mode', mode)
|
|
if ((mode !== 'catalog') && (Conf['Previous Index Mode'] !== mode)) {
|
|
Conf['Previous Index Mode'] = mode
|
|
$.set('Previous Index Mode', mode)
|
|
}
|
|
}
|
|
if ((sort != null) && (sort !== Index.currentSort)) {
|
|
Index.changed.sort = true
|
|
Index.currentSort = sort
|
|
Index.saveSort()
|
|
}
|
|
if (['all pages', 'catalog'].includes(Conf['Index Mode'])) { page = 1 }
|
|
if ((page != null) && (page !== Index.currentPage)) {
|
|
Index.changed.page = true
|
|
Index.currentPage = page
|
|
}
|
|
if (hash != null) {
|
|
return Index.changed.hash = true
|
|
}
|
|
},
|
|
|
|
savePerBoard(key, value) {
|
|
if (typeof Conf[key] === 'object') {
|
|
Conf[key][g.BOARD.ID] = value
|
|
} else {
|
|
Conf[key] = value
|
|
}
|
|
return $.set(key, Conf[key])
|
|
},
|
|
|
|
saveSort() {
|
|
return Index.savePerBoard('Index Sort', Index.currentSort)
|
|
},
|
|
|
|
saveLastLongThresholds(i) {
|
|
return Index.savePerBoard(`Last Long Reply Thresholds ${i}`, Index.lastLongThresholds[i])
|
|
},
|
|
|
|
pageLoad(scroll = true) {
|
|
if (!Index.liveThreadData) { return }
|
|
let { threads, order, search, mode, sort, page, hash } = Index.changed
|
|
if (!threads) { threads = search }
|
|
if (!order) { order = sort }
|
|
if (threads || order) { Index.sort() }
|
|
if (threads) { Index.buildPagelist() }
|
|
if (search) { Index.setupSearch() }
|
|
if (mode) { Index.setupMode() }
|
|
if (sort) { Index.setupSort() }
|
|
if (threads || mode || page || order) { Index.buildIndex() }
|
|
if (threads || page) { Index.setPage() }
|
|
if (scroll && !hash) { Index.scrollToIndex() }
|
|
if (hash) { Header.hashScroll() }
|
|
return Index.changed = {}
|
|
},
|
|
|
|
setupMode() {
|
|
for (const mode of ['paged', 'infinite', 'all pages', 'catalog']) {
|
|
$[mode === Conf['Index Mode'] ? 'addClass' : 'rmClass'](doc, `${mode.replace(/\ /g, '-')}-mode`)
|
|
}
|
|
Index.selectMode.value = Conf['Index Mode']
|
|
Index.cb.size()
|
|
Index.showHiddenThreads = false
|
|
return $('#hidden-toggle a', Index.navLinks).textContent = 'Show'
|
|
},
|
|
|
|
setupSort() {
|
|
Index.selectRev.checked = /-rev$/.test(Index.currentSort)
|
|
Index.selectSort.value = Index.currentSort.replace(/-rev$/, '')
|
|
return Index.lastLongOptions.hidden = (Index.selectSort.value !== 'lastlong')
|
|
},
|
|
|
|
getPagesNum() {
|
|
if (Index.search) {
|
|
return Math.ceil(Index.sortedThreadIDs.length / Index.threadsNumPerPage)
|
|
} else {
|
|
return Index.pagesNum
|
|
}
|
|
},
|
|
|
|
getMaxPageNum() {
|
|
return Math.max(1, Index.getPagesNum())
|
|
},
|
|
|
|
buildPagelist() {
|
|
const pagesRoot = $('.pages', Index.pagelist)
|
|
const maxPageNum = Index.getMaxPageNum()
|
|
if (pagesRoot.childElementCount !== maxPageNum) {
|
|
const nodes = []
|
|
for (let i = 1, end = maxPageNum; i <= end; i++) {
|
|
const a = $.el('a', {
|
|
textContent: i,
|
|
href: i === 1 ? './' : i
|
|
}
|
|
)
|
|
nodes.push($.tn('['), a, $.tn('] '))
|
|
}
|
|
$.rmAll(pagesRoot)
|
|
return $.add(pagesRoot, nodes)
|
|
}
|
|
},
|
|
|
|
setPage() {
|
|
let a, strong
|
|
const pageNum = Index.currentPage
|
|
const maxPageNum = Index.getMaxPageNum()
|
|
const pagesRoot = $('.pages', Index.pagelist)
|
|
|
|
// Previous/Next buttons
|
|
const prev = pagesRoot.previousElementSibling.firstElementChild
|
|
const next = pagesRoot.nextElementSibling.firstElementChild
|
|
let href = Math.max(pageNum - 1, 1)
|
|
prev.href = href === 1 ? './' : href
|
|
prev.firstElementChild.disabled = href === pageNum
|
|
href = Math.min(pageNum + 1, maxPageNum)
|
|
next.href = href === 1 ? './' : href
|
|
next.firstElementChild.disabled = href === pageNum
|
|
|
|
// <strong> current page
|
|
if (strong = $('strong', pagesRoot)) {
|
|
if (+strong.textContent === pageNum) { return }
|
|
$.replace(strong, strong.firstChild)
|
|
} else {
|
|
strong = $.el('strong')
|
|
}
|
|
|
|
if (a = pagesRoot.children[pageNum - 1]) {
|
|
$.before(a, strong)
|
|
return $.add(strong, a)
|
|
}
|
|
},
|
|
|
|
updateHideLabel() {
|
|
if (!Index.hideLabel) { return }
|
|
let hiddenCount = 0
|
|
for (const threadID of Index.liveThreadIDs) {
|
|
if (Index.isHidden(threadID)) {
|
|
hiddenCount++
|
|
}
|
|
}
|
|
if (!hiddenCount) {
|
|
Index.hideLabel.hidden = true
|
|
if (Index.showHiddenThreads) { Index.cb.toggleHiddenThreads() }
|
|
return
|
|
}
|
|
Index.hideLabel.hidden = false
|
|
return $('#hidden-count', Index.navLinks).textContent = hiddenCount === 1 ?
|
|
'1 hidden thread'
|
|
:
|
|
`${hiddenCount} hidden threads`
|
|
},
|
|
|
|
update(firstTime) {
|
|
let oldReq
|
|
if (oldReq = Index.req) {
|
|
delete Index.req
|
|
oldReq.abort()
|
|
}
|
|
|
|
if (Conf['Index Refresh Notifications']) {
|
|
// Optional notification for manual refreshes
|
|
if (!Index.notice) { Index.notice = new Notice('info', 'Refreshing index...') }
|
|
if (!Index.nTimeout) {
|
|
Index.nTimeout = setTimeout(() => {
|
|
if (Index.notice) {
|
|
Index.notice.el.lastElementChild.textContent += ' (disable JSON Index if this takes too long)'
|
|
}
|
|
}
|
|
, 3 * SECOND)
|
|
}
|
|
} else {
|
|
// Also display notice if Index Refresh is taking too long
|
|
if (!Index.nTimeout) {
|
|
Index.nTimeout = setTimeout(() => Index.notice || (Index.notice = new Notice('info', 'Refreshing index... (disable JSON Index if this takes too long)'))
|
|
, 3 * SECOND)
|
|
}
|
|
}
|
|
|
|
// Hard refresh in case of incomplete page load.
|
|
if (!firstTime && (d.readyState !== 'loading') && !$('.board + *')) {
|
|
location.reload()
|
|
return
|
|
}
|
|
|
|
Index.req = $.whenModified(
|
|
g.SITE.urls.catalogJSON({ siteID: g.SITE.siteID, boardID: g.BOARD.boardID }),
|
|
'Index',
|
|
Index.load
|
|
)
|
|
return $.addClass(Index.button, 'fa-spin')
|
|
},
|
|
|
|
load() {
|
|
let err
|
|
if (this !== Index.req) { return } // aborted
|
|
|
|
$.rmClass(Index.button, 'fa-spin')
|
|
const { notice, nTimeout } = Index
|
|
if (nTimeout) { clearTimeout(nTimeout) }
|
|
delete Index.nTimeout
|
|
delete Index.req
|
|
delete Index.notice
|
|
|
|
if (![200, 304].includes(this.status)) {
|
|
err = `Index refresh failed. ${this.status ? `Error ${this.statusText} (${this.status})` : 'Connection Error'}`
|
|
if (notice) {
|
|
notice.setType('warning')
|
|
notice.el.lastElementChild.textContent = err
|
|
setTimeout(notice.close, SECOND)
|
|
} else {
|
|
new Notice('warning', err, 1)
|
|
}
|
|
return
|
|
}
|
|
|
|
try {
|
|
if (this.status === 200) {
|
|
Index.parse(this.response)
|
|
} else if (this.status === 304) {
|
|
Index.pageLoad()
|
|
}
|
|
} catch (error) {
|
|
err = error
|
|
c.error(`Index failure: ${err.message}`, err.stack)
|
|
if (notice) {
|
|
notice.setType('error')
|
|
notice.el.lastElementChild.textContent = 'Index refresh failed.'
|
|
setTimeout(notice.close, SECOND)
|
|
} else {
|
|
new Notice('error', 'Index refresh failed.', 1)
|
|
}
|
|
return
|
|
}
|
|
|
|
if (notice) {
|
|
if (Conf['Index Refresh Notifications']) {
|
|
notice.setType('success')
|
|
notice.el.lastElementChild.textContent = 'Index refreshed!'
|
|
setTimeout(notice.close, SECOND)
|
|
} else {
|
|
notice.close()
|
|
}
|
|
}
|
|
|
|
const timeEl = $('#index-last-refresh time', Index.navLinks)
|
|
timeEl.dataset.utc = Date.parse(this.getResponseHeader('Last-Modified'))
|
|
return RelativeDates.update(timeEl)
|
|
},
|
|
|
|
parse(pages) {
|
|
$.cleanCache(url => /^https?:\/\/a\.4cdn\.org\//.test(url))
|
|
Index.parseThreadList(pages)
|
|
Index.changed.threads = true
|
|
return Index.pageLoad()
|
|
},
|
|
|
|
parseThreadList(pages) {
|
|
Index.pagesNum = pages.length
|
|
Index.threadsNumPerPage = pages[0]?.threads.length || 1
|
|
Index.liveThreadData = pages.reduce(((arr, next) => arr.concat(next.threads)), [])
|
|
Index.liveThreadIDs = Index.liveThreadData.map(data => data.no)
|
|
Index.liveThreadDict = dict()
|
|
Index.threadPosition = dict()
|
|
Index.parsedThreads = dict()
|
|
Index.replyData = dict()
|
|
for (let i = 0; i < Index.liveThreadData.length; i++) {
|
|
var obj, results
|
|
const data = Index.liveThreadData[i]
|
|
Index.liveThreadDict[data.no] = data
|
|
Index.threadPosition[data.no] = i
|
|
Index.parsedThreads[data.no] = (obj = g.SITE.Build.parseJSON(data, g.BOARD))
|
|
obj.filterResults = (results = Filter.test(obj))
|
|
obj.isOnTop = results.top
|
|
obj.isHidden = results.hide || ThreadHiding.isHidden(obj.boardID, obj.threadID)
|
|
if (data.last_replies) {
|
|
for (const reply of data.last_replies) {
|
|
Index.replyData[`${g.BOARD}.${reply.no}`] = reply
|
|
}
|
|
}
|
|
}
|
|
if (Index.liveThreadData[0]) {
|
|
g.SITE.Build.spoilerRange[g.BOARD.ID] = Index.liveThreadData[0].custom_spoiler
|
|
}
|
|
g.BOARD.threads.forEach(function (thread) {
|
|
if (!Index.liveThreadIDs.includes(thread.ID)) { return thread.collect() }
|
|
})
|
|
$.event('IndexUpdate',
|
|
{ threads: ((Index.liveThreadIDs.map((ID) => `${g.BOARD}.${ID}`))) })
|
|
},
|
|
|
|
isHidden(threadID) {
|
|
let thread
|
|
if ((thread = g.BOARD.threads.get(threadID)) && thread.OP && !thread.OP.isFetchedQuote) {
|
|
return thread.isHidden
|
|
} else {
|
|
return Index.parsedThreads[threadID].isHidden
|
|
}
|
|
},
|
|
|
|
isHiddenReply(threadID, replyData) {
|
|
return PostHiding.isHidden(g.BOARD.ID, threadID, replyData.no) || Filter.isHidden(g.SITE.Build.parseJSON(replyData, g.BOARD))
|
|
},
|
|
|
|
buildThreads(threadIDs, isCatalog, withReplies) {
|
|
let errors
|
|
const threads = []
|
|
const newThreads = []
|
|
let newPosts = []
|
|
for (const ID of threadIDs) {
|
|
var opRoot, thread
|
|
try {
|
|
var OP
|
|
const threadData = Index.liveThreadDict[ID]
|
|
|
|
if (thread = g.BOARD.threads.get(ID)) {
|
|
const isStale = (thread.json !== threadData) && (JSON.stringify(thread.json) !== JSON.stringify(threadData))
|
|
if (isStale) {
|
|
thread.setCount('post', threadData.replies + 1, threadData.bumplimit)
|
|
thread.setCount('file', threadData.images + !!threadData.ext, threadData.imagelimit)
|
|
thread.setStatus('Sticky', !!threadData.sticky)
|
|
thread.setStatus('Closed', !!threadData.closed)
|
|
}
|
|
if (thread.catalogView) {
|
|
$.rm(thread.catalogView.nodes.replies)
|
|
thread.catalogView.nodes.replies = null
|
|
}
|
|
} else {
|
|
thread = new Thread(ID, g.BOARD)
|
|
newThreads.push(thread)
|
|
}
|
|
const lastPost = threadData.last_replies && threadData.last_replies.length ? threadData.last_replies[threadData.last_replies.length - 1].no : ID
|
|
if (lastPost > thread.lastPost) { thread.lastPost = lastPost }
|
|
thread.json = threadData
|
|
threads.push(thread)
|
|
|
|
if ((OP = thread.OP) && !OP.isFetchedQuote) {
|
|
OP.setCatalogOP(isCatalog)
|
|
thread.setPage(Math.floor(Index.threadPosition[ID] / Index.threadsNumPerPage) + 1)
|
|
} else {
|
|
const obj = Index.parsedThreads[ID]
|
|
opRoot = g.SITE.Build.post(obj)
|
|
OP = new Post(opRoot, thread, g.BOARD)
|
|
OP.filterResults = obj.filterResults
|
|
newPosts.push(OP)
|
|
}
|
|
|
|
if (!isCatalog || !thread.nodes.root) {
|
|
g.SITE.Build.thread(thread, threadData, withReplies)
|
|
}
|
|
} catch (err) {
|
|
// Skip posts that we failed to parse.
|
|
if (!errors) { errors = [] }
|
|
errors.push({
|
|
message: `Parsing of Thread No.${thread} failed. Thread will be skipped.`,
|
|
error: err,
|
|
html: opRoot?.outerHTML
|
|
})
|
|
}
|
|
}
|
|
if (errors) { Main.handleErrors(errors) }
|
|
|
|
if (withReplies) {
|
|
newPosts = newPosts.concat(Index.buildReplies(threads))
|
|
}
|
|
|
|
Main.callbackNodes('Thread', newThreads)
|
|
Main.callbackNodes('Post', newPosts)
|
|
Index.updateHideLabel()
|
|
$.event('IndexRefreshInternal', { threadIDs: (threads.map((t) => t.fullID)), isCatalog })
|
|
|
|
return threads
|
|
},
|
|
|
|
buildReplies(threads) {
|
|
let errors
|
|
const posts = []
|
|
for (const thread of threads) {
|
|
var lastReplies
|
|
if (!(lastReplies = Index.liveThreadDict[thread.ID].last_replies)) { continue }
|
|
const nodes = []
|
|
for (const data of lastReplies) {
|
|
var node, post
|
|
if ((post = thread.posts.get(data.no)) && !post.isFetchedQuote) {
|
|
nodes.push(post.nodes.root)
|
|
continue
|
|
}
|
|
nodes.push(node = g.SITE.Build.postFromObject(data, thread.board.ID))
|
|
try {
|
|
posts.push(new Post(node, thread, thread.board))
|
|
} catch (err) {
|
|
// Skip posts that we failed to parse.
|
|
if (!errors) { errors = [] }
|
|
errors.push({
|
|
message: `Parsing of Post No.${data.no} failed. Post will be skipped.`,
|
|
error: err,
|
|
html: node?.outerHTML
|
|
})
|
|
}
|
|
}
|
|
$.add(thread.nodes.root, nodes)
|
|
}
|
|
|
|
if (errors) { Main.handleErrors(errors) }
|
|
return posts
|
|
},
|
|
|
|
buildCatalogViews(threads) {
|
|
const catalogThreads = []
|
|
for (const thread of threads) {
|
|
if (!thread.catalogView) {
|
|
const { ID } = thread
|
|
const page = Math.floor(Index.threadPosition[ID] / Index.threadsNumPerPage) + 1
|
|
const root = g.SITE.Build.catalogThread(thread, Index.liveThreadDict[ID], page)
|
|
catalogThreads.push(new CatalogThread(root, thread))
|
|
}
|
|
}
|
|
Main.callbackNodes('CatalogThread', catalogThreads)
|
|
},
|
|
|
|
sizeCatalogViews(threads) {
|
|
// XXX When browsers support CSS3 attr(), use it instead.
|
|
const size = Conf['Index Size'] === 'small' ? 150 : 250
|
|
for (const thread of threads) {
|
|
const { thumb } = thread.catalogView.nodes
|
|
const { width, height } = thumb.dataset
|
|
if (!width) { continue }
|
|
const ratio = size / Math.max(width, height)
|
|
thumb.style.width = (width * ratio) + 'px'
|
|
thumb.style.height = (height * ratio) + 'px'
|
|
}
|
|
},
|
|
|
|
buildCatalogReplies(thread) {
|
|
let lastReplies
|
|
const { nodes } = thread.catalogView
|
|
if (!(lastReplies = Index.liveThreadDict[thread.ID].last_replies)) { return }
|
|
|
|
const replies = []
|
|
for (const data of lastReplies) {
|
|
if (Index.isHiddenReply(thread.ID, data)) { continue }
|
|
const reply = g.SITE.Build.catalogReply(thread, data)
|
|
RelativeDates.update($('time', reply))
|
|
$.on($('.catalog-reply-preview', reply), 'mouseover', QuotePreview.mouseover)
|
|
replies.push(reply)
|
|
}
|
|
|
|
nodes.replies = $.el('div', { className: 'catalog-replies' })
|
|
$.add(nodes.replies, replies)
|
|
$.add(thread.OP.nodes.post, nodes.replies)
|
|
},
|
|
|
|
sort() {
|
|
let threadIDs
|
|
const { liveThreadIDs, liveThreadData } = Index
|
|
if (!liveThreadData) { return }
|
|
const tmp_time = new Date().getTime() / 1000
|
|
const sortType = Index.currentSort.replace(/-rev$/, '')
|
|
Index.sortedThreadIDs = (() => {
|
|
switch (sortType) {
|
|
case 'lastreply': case 'lastlong':
|
|
var repliesAvailable = liveThreadData.some(thread => thread.last_replies?.length)
|
|
var lastlong = function (thread) {
|
|
if (!repliesAvailable) {
|
|
return thread.last_modified
|
|
}
|
|
const iterable = thread.last_replies || []
|
|
for (let i = iterable.length - 1; i >= 0; i--) {
|
|
const r = iterable[i]
|
|
if (Index.isHiddenReply(thread.no, r)) { continue }
|
|
if (sortType === 'lastreply') {
|
|
return r
|
|
}
|
|
const len = r.com ? g.SITE.Build.parseComment(r.com).replace(/[^a-z]/ig, '').length : 0
|
|
if (len >= Index.lastLongThresholds[+!!r.ext]) {
|
|
return r
|
|
}
|
|
}
|
|
if (thread.omitted_posts && thread.last_replies?.length) { return thread.last_replies[0] } else { return thread }
|
|
}
|
|
var lastlongD = dict()
|
|
for (const thread of liveThreadData) {
|
|
lastlongD[thread.no] = lastlong(thread).no
|
|
}
|
|
return [...Array.from(liveThreadData)].sort((a, b) => lastlongD[b.no] - lastlongD[a.no]).map(post => post.no)
|
|
case 'bump': return liveThreadIDs
|
|
case 'birth': return [...Array.from(liveThreadIDs)].sort((a, b) => b - a)
|
|
case 'replycount': return [...Array.from(liveThreadData)].sort((a, b) => b.replies - a.replies).map(post => post.no)
|
|
case 'filecount': return [...Array.from(liveThreadData)].sort((a, b) => b.images - a.images).map(post => post.no)
|
|
case 'activity': return [...Array.from(liveThreadData)].sort((a, b) => ((tmp_time - a.time) / (a.replies + 1)) - ((tmp_time - b.time) / (b.replies + 1))).map(post => post.no)
|
|
default: return liveThreadIDs
|
|
}
|
|
})()
|
|
if (/-rev$/.test(Index.currentSort)) {
|
|
Index.sortedThreadIDs = [...Array.from(Index.sortedThreadIDs)].reverse()
|
|
}
|
|
if (Index.search && (threadIDs = Index.querySearch(Index.search))) {
|
|
Index.sortedThreadIDs = threadIDs
|
|
}
|
|
// Sticky threads
|
|
Index.sortOnTop(obj => obj.isSticky)
|
|
// Highlighted threads
|
|
Index.sortOnTop(obj => obj.isOnTop || (Conf['Pin Watched Threads'] && ThreadWatcher.isWatchedRaw(obj.boardID, obj.threadID)))
|
|
// Non-hidden threads
|
|
if (Conf['Anchor Hidden Threads']) { return Index.sortOnTop(obj => !Index.isHidden(obj.threadID)) }
|
|
},
|
|
|
|
sortOnTop(match) {
|
|
const topThreads = []
|
|
const bottomThreads = []
|
|
for (const ID of Index.sortedThreadIDs) {
|
|
(match(Index.parsedThreads[ID]) ? topThreads : bottomThreads).push(ID)
|
|
}
|
|
return Index.sortedThreadIDs = topThreads.concat(bottomThreads)
|
|
},
|
|
|
|
buildIndex() {
|
|
let threadIDs
|
|
if (!Index.liveThreadData) { return }
|
|
switch (Conf['Index Mode']) {
|
|
case 'all pages':
|
|
threadIDs = Index.sortedThreadIDs
|
|
break
|
|
case 'catalog':
|
|
threadIDs = Index.sortedThreadIDs.filter(ID => !Index.isHidden(ID) !== Index.showHiddenThreads)
|
|
break
|
|
default:
|
|
threadIDs = Index.threadsOnPage(Index.currentPage)
|
|
}
|
|
delete Index.pageNum
|
|
$.rmAll(Index.root)
|
|
$.rmAll(Header.hover)
|
|
if (Index.loaded && Index.root.parentNode) {
|
|
$.event('PostsRemoved', null, Index.root)
|
|
}
|
|
if (Conf['Index Mode'] === 'catalog') {
|
|
Index.buildCatalog(threadIDs)
|
|
} else {
|
|
Index.buildStructure(threadIDs)
|
|
}
|
|
},
|
|
|
|
threadsOnPage(pageNum) {
|
|
const nodesPerPage = Index.threadsNumPerPage
|
|
const offset = nodesPerPage * (pageNum - 1)
|
|
return Index.sortedThreadIDs.slice(offset, offset + nodesPerPage)
|
|
},
|
|
|
|
buildStructure(threadIDs) {
|
|
const threads = Index.buildThreads(threadIDs, false, Conf['Show Replies'])
|
|
const nodes = []
|
|
for (const thread of threads) {
|
|
nodes.push(thread.nodes.root, $.el('hr'))
|
|
}
|
|
$.add(Index.root, nodes)
|
|
if (Index.root.parentNode) {
|
|
$.event('PostsInserted', null, Index.root)
|
|
}
|
|
Index.loaded = true
|
|
},
|
|
|
|
buildCatalog(threadIDs) {
|
|
let i = 0
|
|
const n = threadIDs.length
|
|
let node0 = null
|
|
const fn = function () {
|
|
if (node0 && !node0.parentNode) { return } // Index.root cleared
|
|
const j = (i > 0) && Index.root.parentNode ? n : i + 30
|
|
node0 = Index.buildCatalogPart(threadIDs.slice(i, j))[0]
|
|
i = j
|
|
if (i < n) {
|
|
return $.queueTask(fn)
|
|
} else {
|
|
if (Index.root.parentNode) {
|
|
$.event('PostsInserted', null, Index.root)
|
|
}
|
|
return Index.loaded = true
|
|
}
|
|
}
|
|
fn()
|
|
},
|
|
|
|
buildCatalogPart(threadIDs) {
|
|
const threads = Index.buildThreads(threadIDs, true)
|
|
Index.buildCatalogViews(threads)
|
|
Index.sizeCatalogViews(threads)
|
|
const nodes = []
|
|
for (const thread of threads) {
|
|
thread.OP.setCatalogOP(true)
|
|
$.add(thread.catalogView.nodes.root, thread.OP.nodes.root)
|
|
nodes.push(thread.catalogView.nodes.root)
|
|
$.on(thread.catalogView.nodes.root, 'mouseenter', Index.cb.catalogReplies.bind(thread))
|
|
$.on(thread.OP.nodes.root, 'mouseenter', Index.cb.hoverAdjust.bind(thread.OP.nodes))
|
|
}
|
|
$.add(Index.root, nodes)
|
|
return nodes
|
|
},
|
|
|
|
clearSearch() {
|
|
Index.searchInput.value = ''
|
|
Index.onSearchInput()
|
|
return Index.searchInput.focus()
|
|
},
|
|
|
|
setupSearch() {
|
|
Index.searchInput.value = Index.search
|
|
if (Index.search) {
|
|
return Index.searchInput.dataset.searching = 1
|
|
} else {
|
|
// XXX https://bugzilla.mozilla.org/show_bug.cgi?id=1021289
|
|
return Index.searchInput.removeAttribute('data-searching')
|
|
}
|
|
},
|
|
|
|
onSearchInput() {
|
|
const search = Index.searchInput.value.trim()
|
|
if (search === Index.search) { return }
|
|
Index.pushState({
|
|
search,
|
|
replace: !!search === !!Index.search
|
|
})
|
|
return Index.pageLoad(false)
|
|
},
|
|
|
|
querySearch(query) {
|
|
let keywords, match
|
|
if (match = query.match(/^([\w+]+):\/(.*)\/(\w*)$/)) {
|
|
let regexp
|
|
try {
|
|
regexp = RegExp(match[2], match[3])
|
|
} catch (error) {
|
|
return []
|
|
}
|
|
return Index.sortedThreadIDs.filter(ID => regexp.test(Filter.values(match[1], Index.parsedThreads[ID]).join('\n')))
|
|
}
|
|
if (!(keywords = query.toLowerCase().match(/\S+/g))) { return }
|
|
return Index.sortedThreadIDs.filter(ID => Index.searchMatch(Index.parsedThreads[ID], keywords))
|
|
},
|
|
|
|
searchMatch(obj, keywords) {
|
|
const { info, file } = obj
|
|
if (info.comment == null) { info.comment = g.SITE.Build.parseComment(info.commentHTML.innerHTML) }
|
|
let text = []
|
|
for (const key of ['comment', 'subject', 'name', 'tripcode']) {
|
|
if (key in info) { text.push(info[key]) }
|
|
}
|
|
if (file) { text.push(file.name) }
|
|
text = text.join(' ').toLowerCase()
|
|
for (const keyword of keywords) {
|
|
if (-1 === text.indexOf(keyword.toLowerCase())) { return false }
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
export default Index
|