737 lines
25 KiB
CoffeeScript
737 lines
25 KiB
CoffeeScript
Index =
|
|
showHiddenThreads: false
|
|
changed: {}
|
|
|
|
init: ->
|
|
return if g.BOARD.ID is 'f' or !Conf['JSON Index'] or g.VIEW isnt 'index'
|
|
|
|
CatalogThread.callbacks.push
|
|
name: 'Catalog Features'
|
|
cb: @catalogNode
|
|
|
|
@search = history.state?.searched or ''
|
|
if history.state?.mode
|
|
Conf['Index Mode'] = history.state?.mode
|
|
@currentPage = @getCurrentPage()
|
|
@processHash()
|
|
|
|
$.addClass doc, 'index-loading', "#{Conf['Index Mode'].replace /\ /g, '-'}-mode"
|
|
$.on window, 'popstate', @cb.popstate
|
|
$.on d, 'scroll', Index.scroll
|
|
|
|
# Header refresh button
|
|
@button = $.el 'a',
|
|
className: 'index-refresh-shortcut fa fa-refresh'
|
|
title: 'Refresh'
|
|
href: 'javascript:;'
|
|
textContent: 'Refresh Index'
|
|
$.on @button, 'click', -> Index.update()
|
|
Header.addShortcut @button, 1
|
|
|
|
# Header "Index Navigation" submenu
|
|
repliesEntry = el: UI.checkbox 'Show Replies', 'Show replies'
|
|
pinEntry = el: UI.checkbox 'Pin Watched Threads', 'Pin watched threads'
|
|
anchorEntry = el: UI.checkbox 'Anchor Hidden Threads', 'Anchor hidden threads'
|
|
refNavEntry = el: UI.checkbox 'Refreshed Navigation', 'Refreshed navigation'
|
|
pinEntry.el.title = 'Move watched threads to the start of the index.'
|
|
anchorEntry.el.title = 'Move hidden threads to the end of the index.'
|
|
refNavEntry.el.title = 'Refresh index when navigating through pages.'
|
|
for label in [repliesEntry, pinEntry, anchorEntry, refNavEntry]
|
|
input = label.el.firstChild
|
|
{name} = input
|
|
$.on input, 'change', $.cb.checked
|
|
switch name
|
|
when 'Show Replies'
|
|
$.on input, 'change', @cb.replies
|
|
when 'Pin Watched Threads', 'Anchor Hidden Threads'
|
|
$.on input, 'change', @cb.resort
|
|
|
|
Header.menu.addEntry
|
|
el: $.el 'span',
|
|
textContent: 'Index Navigation'
|
|
order: 100
|
|
subEntries: [repliesEntry, pinEntry, anchorEntry, refNavEntry]
|
|
|
|
# Navigation links at top of index
|
|
@navLinks = $.el 'div', className: 'navLinks json-index'
|
|
$.extend @navLinks, <%= importHTML('General/Index/NavLinks') %>
|
|
$('.cataloglink a', @navLinks).href = CatalogLinks.catalog()
|
|
$('.archlistlink', @navLinks).hidden = true if g.BOARD.ID in ['b', 'trash']
|
|
$.on $('#index-last-refresh a', @navLinks), 'click', @cb.refreshFront
|
|
|
|
# Search field
|
|
@searchInput = $ '#index-search', @navLinks
|
|
@setupSearch()
|
|
$.on @searchInput, 'input', @onSearchInput
|
|
$.on $('#index-search-clear', @navLinks), 'click', @clearSearch
|
|
|
|
# Hidden threads toggle
|
|
@hideLabel = $ '#hidden-label', @navLinks
|
|
$.on $('#hidden-toggle a', @navLinks), 'click', @cb.toggleHiddenThreads
|
|
|
|
# Drop-down menus
|
|
@selectMode = $ '#index-mode', @navLinks
|
|
@selectSort = $ '#index-sort', @navLinks
|
|
@selectSize = $ '#index-size', @navLinks
|
|
$.on @selectMode, 'change', @cb.mode
|
|
$.on @selectSort, 'change', @cb.sort
|
|
$.on @selectSize, 'change', $.cb.value
|
|
$.on @selectSize, 'change', @cb.size
|
|
for select in [@selectMode, @selectSort, @selectSize]
|
|
select.value = Conf[select.name]
|
|
|
|
# Thread container
|
|
@root = $.el 'div', className: 'board json-index'
|
|
@cb.size()
|
|
|
|
# Page list
|
|
@pagelist = $.el 'div', className: 'pagelist json-index'
|
|
$.extend @pagelist, <%= importHTML('General/Index/PageList') %>
|
|
$('.cataloglink a', @pagelist).href = CatalogLinks.catalog()
|
|
$.on @pagelist, 'click', @cb.pageNav
|
|
|
|
@update true
|
|
|
|
$.onExists doc, 'title + *', ->
|
|
d.title = d.title.replace /\ -\ Page\ \d+/, ''
|
|
|
|
$.onExists doc, '.board > .thread > .postContainer, .board + *', ->
|
|
Index.hat = $ '.board > .thread > img:first-child'
|
|
if Index.hat
|
|
if Index.nodes
|
|
for threadRoot in Index.nodes
|
|
$.prepend threadRoot, Index.hat.cloneNode false
|
|
$.addClass doc, 'hats-enabled'
|
|
$.addStyle ".catalog-thread::after {background-image: url(#{Index.hat.src});}"
|
|
|
|
board = $ '.board'
|
|
$.replace board, Index.root
|
|
$.event 'PostsInserted'
|
|
# 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
|
|
|
|
$.rm el for el in $$ '.navLinks'
|
|
$.rm $.id('ctrl-top')
|
|
topNavPos = $.id('delform').previousElementSibling
|
|
$.before topNavPos, $.el 'hr'
|
|
$.before topNavPos, Index.navLinks
|
|
|
|
Main.ready ->
|
|
if (pagelist = $ '.pagelist')
|
|
$.replace pagelist, Index.pagelist
|
|
$.rmClass doc, 'index-loading'
|
|
|
|
scroll: ->
|
|
return if Index.req or !Index.liveThreadData or Conf['Index Mode'] isnt 'infinite' or (window.scrollY <= doc.scrollHeight - (300 + window.innerHeight))
|
|
Index.pageNum ?= Index.currentPage # Avoid having to pushState to keep track of the current page
|
|
|
|
pageNum = ++Index.pageNum
|
|
return Index.endNotice() if pageNum > Index.pagesNum
|
|
|
|
nodes = Index.buildSinglePage pageNum
|
|
Index.buildReplies nodes if Conf['Show Replies']
|
|
Index.buildStructure nodes
|
|
|
|
endNotice: do ->
|
|
notify = false
|
|
reset = -> notify = false
|
|
return ->
|
|
return if notify
|
|
notify = true
|
|
new Notice 'info', "Last page reached.", 2
|
|
setTimeout reset, 3 * $.SECOND
|
|
|
|
menu:
|
|
init: ->
|
|
return if g.VIEW isnt 'index' or !Conf['JSON Index'] or !Conf['Menu'] or !Conf['Thread Hiding Link'] or g.BOARD.ID is 'f'
|
|
|
|
Menu.menu.addEntry
|
|
el: $.el 'a',
|
|
href: 'javascript:;'
|
|
className: 'has-shortcut-text'
|
|
, <%= html('<span></span><span class="shortcut-text">Shift+click</span>') %>
|
|
order: 20
|
|
open: ({thread}) ->
|
|
return false if Conf['Index Mode'] isnt 'catalog'
|
|
@el.firstElementChild.textContent = if thread.isHidden
|
|
'Unhide'
|
|
else
|
|
'Hide'
|
|
$.off @el, 'click', @cb if @cb
|
|
@cb = ->
|
|
$.event 'CloseMenu'
|
|
Index.toggleHide thread
|
|
$.on @el, 'click', @cb
|
|
true
|
|
|
|
catalogNode: ->
|
|
$.on @nodes.thumb.parentNode, 'click', Index.onClick
|
|
|
|
onClick: (e) ->
|
|
return if e.button isnt 0
|
|
thread = g.threads[@parentNode.dataset.fullID]
|
|
if e.shiftKey
|
|
Index.toggleHide thread
|
|
else
|
|
return
|
|
e.preventDefault()
|
|
|
|
toggleHide: (thread) ->
|
|
$.rm thread.catalogView.nodes.root
|
|
if Index.showHiddenThreads
|
|
ThreadHiding.show thread
|
|
return unless ThreadHiding.db.get {boardID: thread.board.ID, threadID: thread.ID}
|
|
# Don't save when un-hiding filtered threads.
|
|
else
|
|
ThreadHiding.hide thread
|
|
ThreadHiding.saveHiddenState thread
|
|
|
|
cycleSortType: ->
|
|
types = [Index.selectSort.options...].filter (option) -> !option.disabled
|
|
for type, i in types
|
|
break if type.selected
|
|
types[(i + 1) % types.length].selected = true
|
|
$.event 'change', null, Index.selectSort
|
|
|
|
cb:
|
|
toggleHiddenThreads: ->
|
|
$('#hidden-toggle a', Index.navLinks).textContent = if Index.showHiddenThreads = !Index.showHiddenThreads
|
|
'Hide'
|
|
else
|
|
'Show'
|
|
Index.sort()
|
|
Index.buildIndex()
|
|
|
|
mode: ->
|
|
Index.pushState {mode: @value}
|
|
Index.pageLoad false
|
|
|
|
sort: ->
|
|
Index.pushState {sort: @value}
|
|
Index.pageLoad false
|
|
|
|
resort: ->
|
|
Index.sort()
|
|
Index.buildIndex()
|
|
|
|
size: (e) ->
|
|
if Conf['Index Mode'] isnt 'catalog'
|
|
$.rmClass Index.root, 'catalog-small'
|
|
$.rmClass Index.root, 'catalog-large'
|
|
else if Conf['Index Size'] is 'small'
|
|
$.addClass Index.root, 'catalog-small'
|
|
$.rmClass Index.root, 'catalog-large'
|
|
else
|
|
$.addClass Index.root, 'catalog-large'
|
|
$.rmClass Index.root, 'catalog-small'
|
|
Index.buildIndex() if e
|
|
|
|
replies: ->
|
|
Index.buildThreads()
|
|
Index.sort()
|
|
Index.buildIndex()
|
|
|
|
popstate: (e) ->
|
|
if e?.state
|
|
{searched, mode, sort} = e.state
|
|
page = Index.getCurrentPage()
|
|
Index.setState {search: searched, mode, sort, page}
|
|
Index.pageLoad false
|
|
else
|
|
# page load or hash change
|
|
if Index.processHash()
|
|
Index[if Conf['Refreshed Navigation'] then 'update' else 'pageLoad']()
|
|
|
|
pageNav: (e) ->
|
|
return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or e.button isnt 0
|
|
switch e.target.nodeName
|
|
when 'BUTTON'
|
|
e.target.blur()
|
|
a = e.target.parentNode
|
|
when 'A'
|
|
a = e.target
|
|
else
|
|
return
|
|
return if a.textContent is 'Catalog'
|
|
e.preventDefault()
|
|
Index.userPageNav +a.pathname.split(/\/+/)[2] or 1
|
|
|
|
refreshFront: ->
|
|
Index.pushState {page: 1}
|
|
Index.update()
|
|
|
|
scrollToIndex: ->
|
|
# Scroll to navlinks, or top of board if navlinks are hidden.
|
|
Header.scrollToIfNeeded (if Index.navLinks.getBoundingClientRect().height then Index.navLinks else Index.root)
|
|
|
|
getCurrentPage: ->
|
|
+window.location.pathname.split(/\/+/)[2] or 1
|
|
|
|
userPageNav: (page) ->
|
|
Index.pushState {page}
|
|
if Conf['Refreshed Navigation']
|
|
Index.update()
|
|
else
|
|
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'
|
|
'creation-date': 'birth'
|
|
'reply-count': 'replycount'
|
|
'file-count': 'filecount'
|
|
|
|
processHash: ->
|
|
# XXX https://bugzilla.mozilla.org/show_bug.cgi?id=483304
|
|
hash = location.href.match(/#.*/)?[0] or ''
|
|
state =
|
|
replace: true
|
|
commands = hash[1..].split '/'
|
|
for command, i in commands
|
|
if (mode = Index.hashCommands.mode[command])
|
|
state.mode = mode
|
|
else if command is 'index'
|
|
state.mode = Conf['Previous Index Mode']
|
|
state.page = 1
|
|
else if (sort = Index.hashCommands.sort[command])
|
|
state.sort = sort
|
|
else if /^s=/.test command
|
|
state.search = decodeURIComponent(command[2..]).replace(/\+/g, ' ').trim()
|
|
else if i is commands.length - 1
|
|
state.hash = if command then "##{command}" else ''
|
|
Index.pushState state
|
|
hash isnt state.hash
|
|
|
|
pushState: (state) ->
|
|
{search, hash, replace} = state
|
|
pageBeforeSearch = history.state?.oldpage
|
|
if search? and search isnt Index.search
|
|
state.page = if search then 1 else (pageBeforeSearch or 1)
|
|
if !search
|
|
pageBeforeSearch = undefined
|
|
else if !Index.search
|
|
pageBeforeSearch = Index.currentPage
|
|
Index.setState state
|
|
pathname = if Index.currentPage is 1 then "/#{g.BOARD}/" else "/#{g.BOARD}/#{Index.currentPage}"
|
|
hash or= ''
|
|
history[if replace then 'replaceState' else 'pushState']
|
|
mode: Conf['Index Mode']
|
|
sort: Conf['Index Sort']
|
|
searched: Index.search
|
|
oldpage: pageBeforeSearch
|
|
, '', "#{location.protocol}//#{location.host}#{pathname}#{hash}"
|
|
|
|
setState: ({search, mode, sort, page}) ->
|
|
if search? and search isnt Index.search
|
|
Index.changed.search = true
|
|
Index.search = search
|
|
if mode? and mode isnt Conf['Index Mode']
|
|
Index.changed.mode = true
|
|
Conf['Index Mode'] = mode
|
|
$.set 'Index Mode', mode
|
|
unless mode is 'catalog' or Conf['Previous Index Mode'] is mode
|
|
Conf['Previous Index Mode'] = mode
|
|
$.set 'Previous Index Mode', mode
|
|
if sort? and sort isnt Conf['Index Sort']
|
|
Index.changed.sort = true
|
|
Conf['Index Sort'] = sort
|
|
$.set 'Index Sort', sort
|
|
page = 1 if Conf['Index Mode'] in ['all pages', 'catalog']
|
|
if page? and page isnt Index.currentPage
|
|
Index.changed.page = true
|
|
Index.currentPage = page
|
|
|
|
pageLoad: (scroll=true) ->
|
|
return unless Index.liveThreadData
|
|
{threads, search, mode, sort, page} = Index.changed
|
|
Index.sort() if threads or search or sort
|
|
Index.buildPagelist() if threads or search
|
|
Index.setupSearch() if search
|
|
Index.setupMode() if mode
|
|
Index.setupSort() if sort
|
|
Index.buildIndex() if threads or search or mode or page or sort
|
|
Index.setPage() if threads or search or mode or page
|
|
Index.scrollToIndex() if scroll
|
|
Index.changed = {}
|
|
|
|
setupMode: ->
|
|
for mode in ['paged', 'infinite', 'all pages', 'catalog']
|
|
$[if mode is Conf['Index Mode'] then 'addClass' else 'rmClass'] doc, "#{mode.replace /\ /g, '-'}-mode"
|
|
Index.selectMode.value = Conf['Index Mode']
|
|
Index.cb.size()
|
|
Index.showHiddenThreads = false
|
|
$('#hidden-toggle a', Index.navLinks).textContent = 'Show'
|
|
|
|
setupSort: ->
|
|
Index.selectSort.value = Conf['Index Sort']
|
|
|
|
getPagesNum: ->
|
|
if Index.search
|
|
Math.ceil Index.sortedNodes.length / Index.threadsNumPerPage
|
|
else
|
|
Index.pagesNum
|
|
|
|
getMaxPageNum: ->
|
|
Math.max 1, Index.getPagesNum()
|
|
|
|
buildPagelist: ->
|
|
pagesRoot = $ '.pages', Index.pagelist
|
|
maxPageNum = Index.getMaxPageNum()
|
|
if pagesRoot.childElementCount isnt maxPageNum
|
|
nodes = []
|
|
for i in [1..maxPageNum] by 1
|
|
a = $.el 'a',
|
|
textContent: i
|
|
href: if i is 1 then './' else i
|
|
nodes.push $.tn('['), a, $.tn '] '
|
|
$.rmAll pagesRoot
|
|
$.add pagesRoot, nodes
|
|
|
|
setPage: ->
|
|
pageNum = Index.currentPage
|
|
maxPageNum = Index.getMaxPageNum()
|
|
pagesRoot = $ '.pages', Index.pagelist
|
|
|
|
# Previous/Next buttons
|
|
prev = pagesRoot.previousSibling.firstChild
|
|
next = pagesRoot.nextSibling.firstChild
|
|
href = Math.max pageNum - 1, 1
|
|
prev.href = if href is 1 then './' else href
|
|
prev.firstChild.disabled = href is pageNum
|
|
href = Math.min pageNum + 1, maxPageNum
|
|
next.href = if href is 1 then './' else href
|
|
next.firstChild.disabled = href is pageNum
|
|
|
|
# <strong> current page
|
|
if strong = $ 'strong', pagesRoot
|
|
return if +strong.textContent is pageNum
|
|
$.replace strong, strong.firstChild
|
|
else
|
|
strong = $.el 'strong'
|
|
|
|
a = pagesRoot.children[pageNum - 1]
|
|
$.before a, strong
|
|
$.add strong, a
|
|
|
|
updateHideLabel: ->
|
|
hiddenCount = 0
|
|
for threadID, thread of g.BOARD.threads when thread.isHidden
|
|
hiddenCount++ if thread.ID in Index.liveThreadIDs
|
|
unless hiddenCount
|
|
Index.hideLabel.hidden = true
|
|
Index.cb.toggleHiddenThreads() if Index.showHiddenThreads
|
|
return
|
|
Index.hideLabel.hidden = false
|
|
$('#hidden-count', Index.navLinks).textContent = if hiddenCount is 1
|
|
'1 hidden thread'
|
|
else
|
|
"#{hiddenCount} hidden threads"
|
|
|
|
update: (firstTime) ->
|
|
Index.req?.abort()
|
|
Index.notice?.close()
|
|
|
|
if Conf['Index Refresh Notifications'] and d.readyState isnt 'loading'
|
|
# Optional notification for manual refreshes
|
|
Index.notice = new Notice 'info', 'Refreshing index...'
|
|
else
|
|
# Also display notice if Index Refresh is taking too long
|
|
now = Date.now()
|
|
$.ready ->
|
|
Index.nTimeout = setTimeout (->
|
|
if Index.req and !Index.notice
|
|
Index.notice = new Notice 'info', 'Refreshing index...'
|
|
), 3 * $.SECOND - (Date.now() - now)
|
|
|
|
# Hard refresh in case of incomplete page load.
|
|
if not firstTime and d.readyState isnt 'loading' and not $('.board + *')
|
|
location.reload()
|
|
return
|
|
|
|
Index.req = $.ajax "//a.4cdn.org/#{g.BOARD}/catalog.json",
|
|
onabort: Index.load
|
|
onloadend: Index.load
|
|
,
|
|
whenModified: 'Index'
|
|
$.addClass Index.button, 'fa-spin'
|
|
|
|
load: (e) ->
|
|
$.rmClass Index.button, 'fa-spin'
|
|
{req, notice, nTimeout} = Index
|
|
clearTimeout nTimeout if nTimeout
|
|
delete Index.nTimeout
|
|
delete Index.req
|
|
delete Index.notice
|
|
|
|
if e.type is 'abort'
|
|
req.onloadend = null
|
|
notice?.close()
|
|
return
|
|
|
|
if req.status not in [200, 304]
|
|
err = "Index refresh failed. Error #{req.statusText} (#{req.status})"
|
|
if notice
|
|
notice.setType 'warning'
|
|
notice.el.lastElementChild.textContent = err
|
|
setTimeout notice.close, $.SECOND
|
|
else
|
|
new Notice 'warning', err, 1
|
|
return
|
|
|
|
try
|
|
if req.status is 200
|
|
Index.parse req.response
|
|
else if req.status is 304
|
|
Index.pageLoad()
|
|
catch err
|
|
c.error "Index failure: #{err.message}", err.stack
|
|
# network error or non-JSON content for example.
|
|
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()
|
|
|
|
timeEl = $ '#index-last-refresh time', Index.navLinks
|
|
timeEl.dataset.utc = Date.parse req.getResponseHeader 'Last-Modified'
|
|
RelativeDates.update timeEl
|
|
|
|
parse: (pages) ->
|
|
$.cleanCache (url) -> /^\/\/a\.4cdn\.org\//.test url
|
|
Index.parseThreadList pages
|
|
Index.buildThreads()
|
|
Index.changed.threads = true
|
|
Index.pageLoad()
|
|
|
|
parseThreadList: (pages) ->
|
|
Index.pagesNum = pages.length
|
|
Index.threadsNumPerPage = pages[0]?.threads.length or 1
|
|
Index.liveThreadData = pages.reduce ((arr, next) -> arr.concat next.threads), []
|
|
Index.liveThreadIDs = Index.liveThreadData.map (data) -> data.no
|
|
g.BOARD.threads.forEach (thread) ->
|
|
thread.collect() unless thread.ID in Index.liveThreadIDs
|
|
return
|
|
|
|
buildThreads: ->
|
|
return unless Index.liveThreadData
|
|
Index.nodes = []
|
|
threads = []
|
|
posts = []
|
|
for threadData, i in Index.liveThreadData
|
|
try
|
|
threadRoot = Build.thread g.BOARD, threadData
|
|
$.prepend threadRoot, Index.hat.cloneNode false if Index.hat
|
|
if thread = g.BOARD.threads[threadData.no]
|
|
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
|
|
else
|
|
thread = new Thread threadData.no, g.BOARD
|
|
threads.push thread
|
|
Index.nodes.push threadRoot
|
|
unless thread.OP and not thread.OP.isFetchedQuote
|
|
posts.push new Post $('.opContainer', threadRoot), thread, g.BOARD
|
|
thread.setPage i // Index.threadsNumPerPage + 1
|
|
catch err
|
|
# Skip posts that we failed to parse.
|
|
errors = [] unless errors
|
|
errors.push
|
|
message: "Parsing of Thread No.#{thread} failed. Thread will be skipped."
|
|
error: err
|
|
Main.handleErrors errors if errors
|
|
|
|
# Add the threads in a container to make sure all features work.
|
|
$.nodes Index.nodes
|
|
Main.callbackNodes Thread, threads
|
|
Main.callbackNodes Post, posts
|
|
Index.updateHideLabel()
|
|
$.event 'IndexRefresh'
|
|
|
|
buildReplies: (threadRoots) ->
|
|
posts = []
|
|
for threadRoot in threadRoots
|
|
thread = Get.threadFromRoot threadRoot
|
|
i = Index.liveThreadIDs.indexOf thread.ID
|
|
continue unless lastReplies = Index.liveThreadData[i].last_replies
|
|
nodes = []
|
|
for data in lastReplies
|
|
if (post = thread.posts[data.no]) and not post.isFetchedQuote
|
|
nodes.push post.nodes.root
|
|
continue
|
|
nodes.push node = Build.postFromObject data, thread.board.ID
|
|
try
|
|
posts.push new Post node, thread, thread.board
|
|
catch err
|
|
# Skip posts that we failed to parse.
|
|
errors = [] unless errors
|
|
errors.push
|
|
message: "Parsing of Post No.#{data.no} failed. Post will be skipped."
|
|
error: err
|
|
$.add threadRoot, nodes
|
|
|
|
Main.handleErrors errors if errors
|
|
Main.callbackNodes Post, posts
|
|
|
|
buildCatalogViews: ->
|
|
threads = Index.sortedNodes
|
|
.map((threadRoot) -> Get.threadFromRoot threadRoot)
|
|
.filter (thread) -> !thread.isHidden isnt Index.showHiddenThreads
|
|
catalogThreads = []
|
|
for thread in threads when !thread.catalogView
|
|
catalogThreads.push new CatalogThread Build.catalogThread(thread), thread
|
|
Main.callbackNodes CatalogThread, catalogThreads
|
|
threads.map (thread) -> thread.catalogView.nodes.root
|
|
|
|
sizeCatalogViews: (nodes) ->
|
|
# XXX When browsers support CSS3 attr(), use it instead.
|
|
size = if Conf['Index Size'] is 'small' then 150 else 250
|
|
for node in nodes
|
|
thumb = $ '.catalog-thumb', node
|
|
{width, height} = thumb.dataset
|
|
continue unless width
|
|
ratio = size / Math.max width, height
|
|
thumb.style.width = width * ratio + 'px'
|
|
thumb.style.height = height * ratio + 'px'
|
|
return
|
|
|
|
sort: ->
|
|
{liveThreadIDs, liveThreadData} = Index
|
|
return unless liveThreadData
|
|
sortedThreadIDs = switch Conf['Index Sort']
|
|
when 'lastreply'
|
|
[liveThreadData...].sort((a, b) ->
|
|
a = num[num.length - 1] if (num = a.last_replies)
|
|
b = num[num.length - 1] if (num = b.last_replies)
|
|
b.no - a.no
|
|
).map (post) -> post.no
|
|
when 'bump' then liveThreadIDs
|
|
when 'birth' then [liveThreadIDs... ].sort (a, b) -> b - a
|
|
when 'replycount' then [liveThreadData...].sort((a, b) -> b.replies - a.replies).map (post) -> post.no
|
|
when 'filecount' then [liveThreadData...].sort((a, b) -> b.images - a.images ).map (post) -> post.no
|
|
Index.sortedNodes = sortedNodes = []
|
|
{nodes} = Index
|
|
for threadID in sortedThreadIDs
|
|
sortedNodes.push nodes[Index.liveThreadIDs.indexOf(threadID)]
|
|
if Index.search and nodes = Index.querySearch(Index.search)
|
|
Index.sortedNodes = nodes
|
|
# Sticky threads
|
|
Index.sortOnTop (thread) -> thread.isSticky
|
|
# Highlighted threads
|
|
Index.sortOnTop (thread) -> thread.isOnTop or Conf['Pin Watched Threads'] and ThreadWatcher.isWatched thread
|
|
# Non-hidden threads
|
|
Index.sortOnTop((thread) -> !thread.isHidden) if Conf['Anchor Hidden Threads']
|
|
|
|
sortOnTop: (match) ->
|
|
topNodes = []
|
|
bottomNodes = []
|
|
for threadRoot in Index.sortedNodes
|
|
(if match Get.threadFromRoot threadRoot then topNodes else bottomNodes).push threadRoot
|
|
Index.sortedNodes = topNodes.concat(bottomNodes)
|
|
|
|
buildIndex: ->
|
|
return unless Index.liveThreadData
|
|
switch Conf['Index Mode']
|
|
when 'all pages'
|
|
nodes = Index.sortedNodes
|
|
when 'catalog'
|
|
nodes = Index.buildCatalogViews()
|
|
Index.sizeCatalogViews nodes
|
|
else
|
|
if Index.followedThreadID?
|
|
i = 0
|
|
i++ while Index.followedThreadID isnt Get.threadFromRoot(Index.sortedNodes[i]).ID
|
|
page = i // Index.threadsNumPerPage + 1
|
|
if page isnt Index.currentPage
|
|
Index.currentPage = page
|
|
Index.pushState {page}
|
|
Index.setPage()
|
|
nodes = Index.buildSinglePage Index.currentPage
|
|
delete Index.pageNum
|
|
$.rmAll Index.root
|
|
$.rmAll Header.hover
|
|
if Conf['Index Mode'] is 'catalog'
|
|
$.add Index.root, nodes
|
|
else
|
|
Index.buildReplies nodes if Conf['Show Replies']
|
|
Index.buildStructure nodes
|
|
if Index.followedThreadID? and (post = g.posts["#{g.BOARD}.#{Index.followedThreadID}"])
|
|
Header.scrollTo post.nodes.root
|
|
|
|
buildSinglePage: (pageNum) ->
|
|
nodesPerPage = Index.threadsNumPerPage
|
|
offset = nodesPerPage * (pageNum - 1)
|
|
Index.sortedNodes.slice offset, offset + nodesPerPage
|
|
|
|
buildStructure: (nodes) ->
|
|
for node in nodes
|
|
if thumb = $ 'img[data-src]', node
|
|
thumb.src = thumb.dataset.src
|
|
# XXX https://bugzilla.mozilla.org/show_bug.cgi?id=1021289
|
|
thumb.removeAttribute 'data-src'
|
|
$.add Index.root, [node, $.el 'hr']
|
|
$.event 'PostsInserted' if doc.contains Index.root
|
|
ThreadHiding.onIndexBuild nodes
|
|
|
|
clearSearch: ->
|
|
Index.searchInput.value = ''
|
|
Index.onSearchInput()
|
|
Index.searchInput.focus()
|
|
|
|
setupSearch: ->
|
|
Index.searchInput.value = Index.search
|
|
if Index.search
|
|
Index.searchInput.dataset.searching = 1
|
|
else
|
|
# XXX https://bugzilla.mozilla.org/show_bug.cgi?id=1021289
|
|
Index.searchInput.removeAttribute 'data-searching'
|
|
|
|
onSearchInput: ->
|
|
search = Index.searchInput.value.trim()
|
|
return if search is Index.search
|
|
Index.pushState
|
|
search: search
|
|
replace: !!search is !!Index.search
|
|
Index.pageLoad false
|
|
|
|
querySearch: (query) ->
|
|
return unless keywords = query.toLowerCase().match /\S+/g
|
|
Index.sortedNodes.filter (threadRoot) ->
|
|
Index.searchMatch Get.threadFromRoot(threadRoot), keywords
|
|
|
|
searchMatch: (thread, keywords) ->
|
|
{info, file} = thread.OP
|
|
text = []
|
|
for key in ['comment', 'subject', 'name', 'tripcode', 'email']
|
|
text.push info[key] if key of info
|
|
text.push file.name if file
|
|
text = text.join(' ').toLowerCase()
|
|
for keyword in keywords
|
|
return false if -1 is text.indexOf keyword
|
|
return true
|