Index = showHiddenThreads: false changed: {} enabledOn: ({siteID, boardID}) -> Conf['JSON Index'] and g.sites[siteID].software is 'yotsuba' and boardID isnt 'f' init: -> return unless g.VIEW is 'index' # For IndexRefresh events $.one d, '4chanXInitFinished', @cb.initFinished $.on d, 'PostsInserted', @cb.postsInserted return unless @enabledOn g.BOARD @enabled = true Callbacks.Post.push name: 'Index Page Numbers' cb: @node Callbacks.CatalogThread.push name: 'Catalog Features' cb: @catalogNode @search = history.state?.searched or '' if history.state?.mode Conf['Index Mode'] = history.state?.mode @currentSort = history.state?.sort @currentSort or= if typeof Conf['Index Sort'] is 'object' then ( Conf['Index Sort'][g.BOARD.ID] or 'bump' ) else ( Conf['Index Sort'] ) @currentPage = @getCurrentPage() @processHash() $.addClass doc, 'index-loading', "#{Conf['Index Mode'].replace /\ /g, '-'}-mode" $.on window, 'popstate', @cb.popstate $.on d, 'scroll', @scroll $.on d, 'SortIndex', @cb.resort # Header refresh button @button = $.el 'a', className: 'fa fa-refresh' title: 'Refresh' href: 'javascript:;' textContent: 'Refresh Index' $.on @button, 'click', -> Index.update() Header.addShortcut 'index-refresh', @button, 590 # Header "Index Navigation" submenu entries = [] @inputs = inputs = {} for name, arr of Config.Index when arr instanceof Array label = UI.checkbox name, "#{name[0]}#{name[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', @cb.replies $.on inputs['Catalog Hover Expand'], 'change', @cb.hover $.on inputs['Pin Watched Threads'], 'change', @cb.resort $.on inputs['Anchor Hidden Threads'], 'change', @cb.resort watchSettings = (e) -> if (input = inputs[e.target.name]) input.checked = e.target.checked $.event 'change', null, input $.on d, 'OpenSettings', -> $.on $.id('fourchanx-settings'), 'change', watchSettings sortEntry = UI.checkbox 'Per-Board Sort Type', 'Per-board sort type', (typeof Conf['Index Sort'] is 'object') sortEntry.title = 'Set the sorting order of each board independently.' $.on sortEntry.firstChild, 'change', @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 @navLinks = $.el 'div', className: 'navLinks json-index' $.extend @navLinks, `<%= readHTML('NavLinks.html') %>` $('.cataloglink a', @navLinks).href = CatalogLinks.catalog() $('.archlistlink', @navLinks).hidden = true unless BoardConfig.isArchived(g.BOARD.ID) $.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 and reverse sort toggle @selectRev = $ '#index-rev', @navLinks @selectMode = $ '#index-mode', @navLinks @selectSort = $ '#index-sort', @navLinks @selectSize = $ '#index-size', @navLinks $.on @selectRev, 'change', @cb.sort $.on @selectMode, 'change', @cb.mode $.on @selectSort, 'change', @cb.sort $.on @selectSize, 'change', $.cb.value $.on @selectSize, 'change', @cb.size for select in [@selectMode, @selectSize] select.value = Conf[select.name] @selectRev.checked = /-rev$/.test Index.currentSort @selectSort.value = Index.currentSort.replace /-rev$/, '' # Last Long Reply options @lastLongOptions = $ '#lastlong-options', @navLinks @lastLongInputs = $$ 'input', @lastLongOptions @lastLongThresholds = [0, 0] @lastLongOptions.hidden = (@selectSort.value isnt 'lastlong') for input, i in @lastLongInputs $.on input, 'change', @cb.lastLongThresholds tRaw = Conf["Last Long Reply Thresholds #{i}"] input.value = @lastLongThresholds[i] = if typeof tRaw is 'object' then (tRaw[g.BOARD.ID] ? 100) else tRaw # Thread container @root = $.el 'div', className: 'board json-index' $.on @root, 'click', @cb.hoverToggle @cb.size() @cb.hover() # Page list @pagelist = $.el 'div', className: 'pagelist json-index' $.extend @pagelist, `<%= readHTML('PageList.html') %>` $('.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 + *', -> g.SITE.Build.hat = $ '.board > .thread > img:first-child' if g.SITE.Build.hat g.BOARD.threads.forEach (thread) -> if thread.nodes.root $.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});}" 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 $.rm el for el in $$ '.navLinks' $.rm $.id('ctrl-top') topNavPos = $.id('delform').previousElementSibling $.before topNavPos, $.el 'hr' $.before topNavPos, Index.navLinks timeEl = $ '#index-last-refresh time', Index.navLinks RelativeDates.update timeEl if timeEl.dataset.utc 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 threadIDs = Index.threadsOnPage pageNum Index.buildStructure threadIDs 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 unless g.VIEW is 'index' and Conf['Menu'] and Conf['Thread Hiding Link'] and Index.enabledOn(g.BOARD) Menu.menu.addEntry el: $.el 'a', href: 'javascript:;' className: 'has-shortcut-text' , `<%= html('Shift+click') %>` 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 node: -> return if @isReply or @isClone or not (Index.threadPosition[@ID]?) @thread.setPage(Index.threadPosition[@ID] // Index.threadsNumPerPage + 1) catalogNode: -> $.on @nodes.root, 'mousedown click', (e) => return unless e.button is 0 and e.shiftKey Index.toggleHide @thread if e.type is 'click' e.preventDefault() # Also on mousedown to prevent highlighting text. toggleHide: (thread) -> 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: initFinished: -> Index.initFinishedFired = true $.queueTask -> Index.cb.postsInserted() postsInserted: -> return unless Index.initFinishedFired n = 0 g.posts.forEach (post) -> if !post.isFetchedQuote and !post.indexRefreshSeen and doc.contains(post.nodes.root) post.indexRefreshSeen = true n++ $.event 'IndexRefresh' if n 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: -> value = if Index.selectRev.checked then Index.selectSort.value + "-rev" else Index.selectSort.value Index.pushState {sort: value} Index.pageLoad false resort: (e) -> Index.changed.order = true Index.pageLoad false unless e?.detail?.deferred perBoardSort: -> Conf['Index Sort'] = if @checked then {} else '' Index.saveSort() for i in [0...2] Conf["Last Long Reply Thresholds #{i}"] = if @checked then {} else '' Index.saveLastLongThresholds i return lastLongThresholds: -> i = [@parentNode.children...].indexOf @ value = +@value unless Number.isFinite(value) @value = Index.lastLongThresholds[i] return Index.lastLongThresholds[i] = value Index.saveLastLongThresholds i Index.changed.order = true Index.pageLoad false 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.buildIndex() hover: -> doc.classList.toggle 'catalog-hover-expand', Conf['Catalog Hover Expand'] hoverToggle: (e) -> if Conf['Catalog Hover Toggle'] and $.hasClass(doc, 'catalog-mode') and !$.modifiedClick(e) and !$.x('ancestor-or-self::a', e.target) 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 Index.cb.hoverAdjust.call thread.OP.nodes 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 nCommands = Index.processHash() if Conf['Refreshed Navigation'] and nCommands Index.update() else Index.pageLoad() pageNav: (e) -> return if $.modifiedClick e 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() catalogReplies: -> if Conf['Show Replies'] and $.hasClass(doc, 'catalog-hover-expand') and !@catalogView.nodes.replies Index.buildCatalogReplies @ hoverAdjust: -> # Prevent hovered catalog threads from going offscreen. return unless $.hasClass(doc, 'catalog-hover-expand') rect = @post.getBoundingClientRect() if (x = $.minmax 0, -rect.left, doc.clientWidth - rect.right) {style} = @post style.left = "#{x}px" style.right = "#{-x}px" $.one @root, 'mouseleave', -> style.left = style.right = null 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' 'last-long-reply': 'lastlong' '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 '/' leftover = [] for command 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.replace(/-rev$/, '')]) state.sort = sort state.sort += '-rev' if /-rev$/.test(command) else if /^s=/.test command state.search = decodeURIComponent(command[2..]).replace(/\+/g, ' ').trim() else leftover.push command hash = leftover.join '/' state.hash = "##{hash}" if hash Index.pushState state commands.length - leftover.length 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: Index.currentSort searched: Index.search oldpage: pageBeforeSearch , '', "#{location.protocol}//#{location.host}#{pathname}#{hash}" setState: ({search, mode, sort, page, hash}) -> 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 Index.currentSort Index.changed.sort = true Index.currentSort = sort Index.saveSort() page = 1 if Conf['Index Mode'] in ['all pages', 'catalog'] if page? and page isnt Index.currentPage Index.changed.page = true Index.currentPage = page if hash? Index.changed.hash = true savePerBoard: (key, value) -> if typeof Conf[key] is 'object' Conf[key][g.BOARD.ID] = value else Conf[key] = value $.set key, Conf[key] saveSort: -> Index.savePerBoard 'Index Sort', Index.currentSort saveLastLongThresholds: (i) -> Index.savePerBoard "Last Long Reply Thresholds #{i}", Index.lastLongThresholds[i] pageLoad: (scroll=true) -> return unless Index.liveThreadData {threads, order, search, mode, sort, page, hash} = Index.changed threads or= search order or= sort Index.sort() if threads or order Index.buildPagelist() if threads Index.setupSearch() if search Index.setupMode() if mode Index.setupSort() if sort Index.buildIndex() if threads or mode or page or order Index.setPage() if threads or page Index.scrollToIndex() if scroll and not hash Header.hashScroll() if hash 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.selectRev.checked = /-rev$/.test Index.currentSort Index.selectSort.value = Index.currentSort.replace /-rev$/, '' Index.lastLongOptions.hidden = (Index.selectSort.value isnt 'lastlong') getPagesNum: -> if Index.search Math.ceil Index.sortedThreadIDs.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 # current page if strong = $ 'strong', pagesRoot return if +strong.textContent is pageNum $.replace strong, strong.firstChild else strong = $.el 'strong' if (a = pagesRoot.children[pageNum - 1]) $.before a, strong $.add strong, a updateHideLabel: -> return unless Index.hideLabel hiddenCount = 0 for threadID in Index.liveThreadIDs when Index.isHidden(threadID) hiddenCount++ 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) -> if (oldReq = Index.req) delete Index.req oldReq.abort() if Conf['Index Refresh Notifications'] # Optional notification for manual refreshes Index.notice or= new Notice 'info', 'Refreshing index...' else # Also display notice if Index Refresh is taking too long Index.nTimeout or= setTimeout -> Index.notice or= new Notice 'info', 'Refreshing index...' , 3 * $.SECOND # Hard refresh in case of incomplete page load. if not firstTime and d.readyState isnt 'loading' and not $('.board + *') location.reload() return Index.req = $.whenModified( g.SITE.urls.catalogJSON({boardID: g.BOARD.ID}), 'Index', Index.load ) $.addClass Index.button, 'fa-spin' load: -> return if @ isnt Index.req # aborted $.rmClass Index.button, 'fa-spin' {notice, nTimeout} = Index clearTimeout nTimeout if nTimeout delete Index.nTimeout delete Index.req delete Index.notice if @status not in [200, 304] err = "Index refresh failed. #{if @status then "Error #{@statusText} (#{@status})" else '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 @status is 200 Index.parse @response else if @status is 304 Index.pageLoad() catch err 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() timeEl = $ '#index-last-refresh time', Index.navLinks timeEl.dataset.utc = Date.parse @getResponseHeader 'Last-Modified' RelativeDates.update timeEl parse: (pages) -> $.cleanCache (url) -> /^https?:\/\/a\.4cdn\.org\//.test url Index.parseThreadList pages 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 Index.liveThreadDict = {} Index.threadPosition = {} Index.parsedThreads = {} Index.replyData = {} for data, i in Index.liveThreadData 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 or ThreadHiding.isHidden(obj.boardID, obj.threadID) if data.last_replies for reply in 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 (thread) -> (thread.collect() unless thread.ID in Index.liveThreadIDs) $.event 'IndexUpdate', threads: ("#{g.BOARD}.#{ID}" for ID in Index.liveThreadIDs) return isHidden: (threadID) -> if (thread = g.BOARD.threads[threadID]) and thread.OP and not thread.OP.isFetchedQuote thread.isHidden else Index.parsedThreads[threadID].isHidden isHiddenReply: (threadID, replyData) -> PostHiding.isHidden(g.BOARD.ID, threadID, replyData.no) or Filter.isHidden(g.SITE.Build.parseJSON replyData, g.BOARD) buildThreads: (threadIDs, isCatalog, withReplies) -> threads = [] newThreads = [] newPosts = [] for ID in threadIDs try threadData = Index.liveThreadDict[ID] if (thread = g.BOARD.threads[ID]) isStale = (thread.json isnt threadData) and (JSON.stringify(thread.json) isnt 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 lastPost = if threadData.last_replies then threadData.last_replies[threadData.last_replies.length - 1].no else ID thread.lastPost = lastPost if lastPost > thread.lastPost thread.json = threadData threads.push thread if ((OP = thread.OP) and not OP.isFetchedQuote) OP.setCatalogOP isCatalog thread.setPage(Index.threadPosition[ID] // Index.threadsNumPerPage + 1) else obj = Index.parsedThreads[ID] OP = new Post g.SITE.Build.post(obj), thread, g.BOARD OP.filterResults = obj.filterResults newPosts.push OP unless isCatalog and thread.nodes.root g.SITE.Build.thread thread, threadData, withReplies 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 if withReplies newPosts = newPosts.concat(Index.buildReplies threads) Main.callbackNodes 'Thread', newThreads Main.callbackNodes 'Post', newPosts Index.updateHideLabel() $.event 'IndexRefreshInternal', {threadIDs: (t.fullID for t in threads), isCatalog} threads buildReplies: (threads) -> posts = [] for thread in threads continue if not (lastReplies = Index.liveThreadDict[thread.ID].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 = 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. errors = [] unless errors errors.push message: "Parsing of Post No.#{data.no} failed. Post will be skipped." error: err $.add thread.nodes.root, nodes Main.handleErrors errors if errors posts buildCatalogViews: (threads) -> catalogThreads = [] for thread in threads when !thread.catalogView {ID} = thread page = Index.threadPosition[ID] // Index.threadsNumPerPage + 1 root = g.SITE.Build.catalogThread thread, Index.liveThreadDict[ID], page catalogThreads.push new CatalogThread root, thread Main.callbackNodes 'CatalogThread', catalogThreads return sizeCatalogViews: (threads) -> # XXX When browsers support CSS3 attr(), use it instead. size = if Conf['Index Size'] is 'small' then 150 else 250 for thread in threads {thumb} = thread.catalogView.nodes {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 buildCatalogReplies: (thread) -> {nodes} = thread.catalogView return if not (lastReplies = Index.liveThreadDict[thread.ID].last_replies) replies = [] for data in lastReplies continue if Index.isHiddenReply thread.ID, data 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 return sort: -> {liveThreadIDs, liveThreadData} = Index return unless liveThreadData sortType = Index.currentSort.replace(/-rev$/, '') Index.sortedThreadIDs = switch sortType when 'lastreply', 'lastlong' lastlong = (thread) -> for r, i in (thread.last_replies or []) by -1 continue if Index.isHiddenReply thread.no, r if sortType is 'lastreply' return r len = if r.com then g.SITE.Build.parseComment(r.com).replace(/[^a-z]/ig, '').length else 0 if len >= Index.lastLongThresholds[+!!r.ext] return r if thread.omitted_posts then thread.last_replies[0] else thread lastlongD = {} for thread in liveThreadData lastlongD[thread.no] = lastlong(thread).no [liveThreadData...].sort((a, b) -> lastlongD[b.no] - lastlongD[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 else liveThreadIDs if /-rev$/.test(Index.currentSort) Index.sortedThreadIDs = [Index.sortedThreadIDs...].reverse() if Index.search and (threadIDs = Index.querySearch Index.search) Index.sortedThreadIDs = threadIDs # Sticky threads Index.sortOnTop (obj) -> obj.isSticky # Highlighted threads Index.sortOnTop (obj) -> obj.isOnTop or Conf['Pin Watched Threads'] and ThreadWatcher.isWatchedRaw(obj.boardID, obj.threadID) # Non-hidden threads Index.sortOnTop((obj) -> !Index.isHidden(obj.threadID)) if Conf['Anchor Hidden Threads'] sortOnTop: (match) -> topThreads = [] bottomThreads = [] for ID in Index.sortedThreadIDs (if match Index.parsedThreads[ID] then topThreads else bottomThreads).push ID Index.sortedThreadIDs = topThreads.concat bottomThreads buildIndex: -> return unless Index.liveThreadData switch Conf['Index Mode'] when 'all pages' threadIDs = Index.sortedThreadIDs when 'catalog' threadIDs = Index.sortedThreadIDs.filter (ID) -> !Index.isHidden(ID) isnt Index.showHiddenThreads else threadIDs = Index.threadsOnPage Index.currentPage delete Index.pageNum $.rmAll Index.root $.rmAll Header.hover if Index.loaded and Index.root.parentNode $.event 'PostsRemoved', null, Index.root if Conf['Index Mode'] is 'catalog' Index.buildCatalog threadIDs else Index.buildStructure threadIDs return threadsOnPage: (pageNum) -> nodesPerPage = Index.threadsNumPerPage offset = nodesPerPage * (pageNum - 1) Index.sortedThreadIDs[offset ... offset + nodesPerPage] buildStructure: (threadIDs) -> threads = Index.buildThreads threadIDs, false, Conf['Show Replies'] nodes = [] for thread in threads nodes.push thread.nodes.root, $.el('hr') $.add Index.root, nodes if Index.root.parentNode $.event 'PostsInserted', null, Index.root Index.loaded = true return buildCatalog: (threadIDs) -> i = 0 n = threadIDs.length node0 = null fn = -> return if node0 and !node0.parentNode # Index.root cleared j = if i > 0 and Index.root.parentNode then n else i + 30 node0 = Index.buildCatalogPart(threadIDs[i...j])[0] i = j if i < n $.queueTask fn else if Index.root.parentNode $.event 'PostsInserted', null, Index.root Index.loaded = true fn() return buildCatalogPart: (threadIDs) -> threads = Index.buildThreads threadIDs, true Index.buildCatalogViews threads Index.sizeCatalogViews threads nodes = [] for thread in 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 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) -> if (match = query.match /^([\w+]+):\/(.*)\/(\w*)$/) try regexp = RegExp match[2], match[3] catch return [] return Index.sortedThreadIDs.filter (ID) -> regexp.test(Filter.values(match[1], Index.parsedThreads[ID]).join('\n')) return if not (keywords = query.toLowerCase().match /\S+/g) Index.sortedThreadIDs.filter (ID) -> Index.searchMatch Index.parsedThreads[ID], keywords searchMatch: (obj, keywords) -> {info, file} = obj info.comment ?= g.SITE.Build.parseComment info.commentHTML.innerHTML text = [] for key in ['comment', 'subject', 'name', 'tripcode'] 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