Index = showHiddenThreads: false init: -> return if g.BOARD.ID is 'f' or !Conf['JSON Navigation'] @board = "#{g.BOARD}" @button = $.el 'a', className: 'index-refresh-shortcut fa fa-refresh' title: 'Refresh' href: 'javascript:;' textContent: 'Refresh Index' $.on @button, 'click', @update Header.addShortcut @button, 1 return if g.BOARD.ID is 'f' @db = new DataBoard 'pinnedThreads' Thread.callbacks.push name: 'Thread Pinning' cb: @threadNode CatalogThread.callbacks.push name: 'Catalog Features' cb: @catalogNode modeEntry = el: $.el 'span', textContent: 'Index mode' subEntries: [ { el: $.el 'label', innerHTML: ' Paged' } { el: $.el 'label', innerHTML: ' Infinite scrolling' } { el: $.el 'label', innerHTML: ' All threads' } ] for label in modeEntry.subEntries input = label.el.firstChild input.checked = Conf['Index Mode'] is input.value $.on input, 'change', $.cb.value $.on input, 'change', @cb.mode sortEntry = el: $.el 'span', textContent: 'Sort by' threadNumEntry = el: $.el 'span', textContent: 'Threads per page' subEntries: [ { el: $.el 'label', innerHTML: '', title: 'Use 0 for default value' } ] threadsNumInput = threadNumEntry.subEntries[0].el.firstChild threadsNumInput.value = Conf['Threads per Page'] $.on threadsNumInput, 'change', $.cb.value $.on threadsNumInput, 'change', @cb.threadsNum targetEntry = el: $.el 'label', innerHTML: ' Open threads in a new tab' title: 'Catalog-only setting.' repliesEntry = el: $.el 'label', innerHTML: ' Show replies' refNavEntry = el: $.el 'label', innerHTML: ' Refreshed navigation' title: 'Refresh index when navigating through pages.' for label in [targetEntry, repliesEntry, refNavEntry] input = label.el.firstChild {name} = input input.checked = Conf[name] $.on input, 'change', $.cb.checked switch name when 'Open threads in a new tab' $.on input, 'change', @cb.target when 'Show Replies' $.on input, 'change', @cb.replies $.event 'AddMenuEntry', type: 'header' el: $.el 'span', textContent: 'Index Navigation' order: 98 subEntries: [threadNumEntry, targetEntry, repliesEntry, refNavEntry] $.addClass doc, 'index-loading' @root = $.el 'div', className: 'board' @pagelist = $.el 'div', className: 'pagelist' hidden: true innerHTML: <%= importHTML('Features/Index-pagelist') %> @navLinks = $.el 'div', className: 'navLinks' innerHTML: <%= importHTML('Features/Index-navlinks') %> @searchInput = $ '#index-search', @navLinks @hideLabel = $ '#hidden-label', @navLinks @selectMode = $ '#index-mode', @navLinks @selectSort = $ '#index-sort', @navLinks @selectSize = $ '#index-size', @navLinks $.on @searchInput, 'input', @onSearchInput $.on $('#index-search-clear', @navLinks), 'click', @clearSearch $.on $('#hidden-toggle a', @navLinks), 'click', @cb.toggleHiddenThreads for select in [@selectMode, @selectSort, @selectSize] select.value = Conf[select.name] $.on select, 'change', $.cb.value $.on @selectMode, 'change', @cb.mode $.on @selectSort, 'change', @cb.sort $.on @selectSize, 'change', @cb.size @searchInput = $ '#index-search', @navLinks @currentPage = @getCurrentPage() $.on d, 'scroll', Index.scroll $.on @pagelist, 'click', @cb.pageNav $.on @searchInput, 'input', @onSearchInput $.on $('#index-search-clear', @navLinks), 'click', @clearSearch $.on $('#returnlink a', @navLinks), 'click', Navigate.navigate @update() if g.VIEW is 'index' $.asap (-> $('.board', doc) or d.readyState isnt 'loading'), -> $.rm navLink for navLink in $$ '.navLinks' $.after $.x('child::form/preceding-sibling::hr[1]'), Index.navLinks return g.VIEW isnt 'index' board = $ '.board' $.replace board, 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 d.implementation.createDocument(null, null, null).appendChild board @cb.toggleCatalogMode() $.asap (-> $('.pagelist', doc) or d.readyState isnt 'loading'), -> if pagelist = $('.pagelist') $.replace pagelist, Index.pagelist else $.after $.id('delform'), Index.pagelist $.rmClass doc, 'index-loading' scroll: $.debounce 100, -> return if Index.req or Conf['Index Mode'] isnt 'infinite' or (doc.scrollTop <= doc.scrollHeight - (300 + window.innerHeight)) or g.VIEW is 'thread' Index.pageNum = (Index.pageNum or Index.getCurrentPage()) + 1 # Avoid having to pushState to keep track of the current page return Index.endNotice() if Index.pageNum >= Index.pagesNum Index.buildIndex true 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['Menu'] or g.BOARD.ID is 'f' $.event 'AddMenuEntry', type: 'post' el: $.el 'a', href: 'javascript:;' order: 19 open: ({thread}) -> return false if Conf['Index Mode'] isnt 'catalog' @el.textContent = if thread.isPinned 'Unpin thread' else 'Pin thread' $.off @el, 'click', @cb if @cb @cb = -> $.event 'CloseMenu' Index.togglePin thread $.on @el, 'click', @cb true threadNode: -> return if g.VIEW isnt 'index' return unless Index.db.get {boardID: @board.ID, threadID: @ID} @pin() catalogNode: -> $.on @nodes.thumb, 'click', Index.onClick return if Conf['Image Hover in Catalog'] $.on @nodes.thumb, 'mouseover', Index.onOver onClick: (e) -> return if e.button isnt 0 thread = g.threads[@parentNode.dataset.fullID] if e.shiftKey PostHiding.toggle thread.OP else if e.altKey Index.togglePin thread else Navigate.navigate.call @ e.preventDefault() onOver: (e) -> # 4chan's less than stellar CSS forces us to include a .post and .postInfo # in order to have proper styling for the .nameBlock's content. {nodes} = g.threads[@parentNode.dataset.fullID].OP el = $.el 'div', innerHTML: '
' className: 'thread-info dialog' hidden: true $.add el.firstElementChild.firstElementChild, [ $('.nameBlock', nodes.info).cloneNode true $.tn ' ' nodes.date.cloneNode true ] $.add Header.hover, el UI.hover root: @ el: el latestEvent: e endEvents: 'mouseout' offsetX: 15 offsetY: -20 setTimeout (-> el.hidden = false if el.parentNode), .25 * $.SECOND togglePin: (thread) -> data = boardID: thread.board.ID threadID: thread.ID if thread.isPinned thread.unpin() Index.db.delete data else thread.pin() data.val = true Index.db.set data Index.sort() Index.buildIndex() setIndexMode: (mode) -> Index.selectMode.value = mode $.event 'change', null, Index.selectMode 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 addCatalogSwitch: -> a = $.el 'a', href: 'javascript:;' textContent: 'Switch to <%= meta.name %>\'s catalog' className: 'btn-wrap' $.on a, 'click', -> $.set 'Index Mode', 'catalog' window.location = './' $.add $.id('info'), a setupNavLinks: -> for el in $$ '.navLinks.desktop > a' if el.getAttribute('href') is '.././catalog' el.href = '.././' $.on el, 'click', -> switch @textContent when 'Return' $.set 'Index Mode', Conf['Previous Index Mode'] when 'Catalog' $.set 'Index Mode', 'catalog' return cb: toggleCatalogMode: -> if Conf['Index Mode'] is 'catalog' $.addClass doc, 'catalog-mode' else $.rmClass doc, 'catalog-mode' Index.cb.size() toggleHiddenThreads: -> $('#hidden-toggle a', Index.navLinks).textContent = if Index.showHiddenThreads = !Index.showHiddenThreads 'Hide' else 'Show' Index.sort() if Conf['Index Mode'] is 'paged' and Index.getCurrentPage() > 0 Index.pageNav 0 else Index.buildIndex() mode: (e) -> Index.cb.toggleCatalogMode() Index.togglePagelist() Index.buildIndex() if e mode = Conf['Index Mode'] if mode not in ['catalog', Conf['Previous Index Mode']] Conf['Previous Index Mode'] = mode $.set 'Previous Index Mode', mode sort: (e) -> Index.sort() Index.buildIndex() if e 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 threadsNum: -> return unless Conf['Index Mode'] is 'paged' Index.buildIndex() target: -> for threadID, thread of g.BOARD.threads when thread.catalogView {thumb} = thread.catalogView.nodes if Conf['Open threads in a new tab'] thumb.target = '_blank' else thumb.removeAttribute 'target' return replies: -> Index.buildThreads() Index.sort() Index.buildIndex() 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' a = e.target.parentNode when 'A' a = e.target else return e.preventDefault() return if Index.cb.indexNav a, true Index.userPageNav +a.pathname.split('/')[2] headerNav: (e) -> a = e.target return if e.button isnt 0 or a.nodeName isnt 'A' or a.hostname isnt 'boards.4chan.org' # Save settings onSameIndex = g.VIEW is 'index' and a.pathname.split('/')[1] is g.BOARD.ID needChange = Index.cb.indexNav a, onSameIndex # Do nav if this isn't a simple click, or different board. return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or !onSameIndex e.preventDefault() Index.update() unless needChange indexNav: (a, onSameIndex) -> {indexMode, indexSort} = a.dataset if indexMode and Conf['Index Mode'] isnt indexMode $.set 'Index Mode', indexMode Conf['Index Mode'] = indexMode if onSameIndex Index.selectMode.value = indexMode Index.cb.mode() needChange = true if indexSort and Conf['Index Sort'] isnt indexSort $.set 'Index Sort', indexSort Conf['Index Sort'] = indexSort if onSameIndex Index.selectSort.value = indexSort Index.cb.sort() needChange = true if needChange Index.buildIndex() Index.scrollToIndex() needChange scrollToIndex: -> Header.scrollToIfNeeded Index.navLinks getCurrentPage: -> if Conf['Index Mode'] is 'infinite' and Index.pageNum return Index.pageNum +window.location.pathname.split('/')[2] userPageNav: (pageNum) -> if Conf['Refreshed Navigation'] and Conf['Index Mode'] isnt 'all pages' Index.update pageNum else Index.pageNav pageNum pageNav: (pageNum) -> return if Index.currentPage is pageNum history.pushState null, '', if pageNum is 0 then './' else pageNum Index.pageLoad pageNum pageLoad: (pageNum) -> Index.currentPage = pageNum return if Conf['Index Mode'] is 'all pages' Index.buildIndex() Index.scrollToIndex() getThreadsNumPerPage: -> if Conf['Threads per Page'] > 0 +Conf['Threads per Page'] else Index.threadsNumPerPage getPagesNum: -> Math.ceil Index.sortedThreads.length / Index.getThreadsNumPerPage() getMaxPageNum: -> Math.max 0, Index.getPagesNum() - 1 togglePagelist: -> Index.pagelist.hidden = Conf['Index Mode'] isnt 'paged' buildPagelist: -> pagesRoot = $ '.pages', Index.pagelist maxPageNum = Index.getMaxPageNum() if pagesRoot.childElementCount isnt maxPageNum + 1 nodes = [] for i in [0..maxPageNum] by 1 a = $.el 'a', textContent: i href: if i then i else './' nodes.push $.tn('['), a, $.tn '] ' $.rmAll pagesRoot $.add pagesRoot, nodes Index.togglePagelist() setPage: (pageNum) -> pageNum or= Index.getCurrentPage() maxPageNum = Index.getMaxPageNum() pagesRoot = $ '.pages', Index.pagelist # Previous/Next buttons prev = pagesRoot.previousSibling.firstChild next = pagesRoot.nextSibling.firstChild href = Math.max pageNum - 1, 0 prev.href = if href is 0 then './' else href prev.firstChild.disabled = href is pageNum href = Math.min pageNum + 1, maxPageNum next.href = if href is 0 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' a = pagesRoot.children[pageNum] $.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.hideLabel).textContent = if hiddenCount is 1 '1 hidden thread' else "#{hiddenCount} hidden threads" update: (pageNum) -> return unless navigator.onLine if g.VIEW is 'thread' return ThreadUpdater.update() if Conf['Thread Updater'] return unless d.readyState is 'loading' or Index.root.parentElement $.replace $('.board'), Index.root delete Index.pageNum Index.req?.abort() Index.notice?.close() # This notice only displays 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...', 2 ), 3 * $.SECOND - (Date.now() - now) pageNum = null if typeof pageNum isnt 'number' # event onload = (e) -> Index.load e, pageNum Index.req = $.ajax "//a.4cdn.org/#{g.BOARD}/catalog.json", onabort: onload onloadend: onload , whenModified: Index.board is "#{g.BOARD}" $.addClass Index.button, 'fa-spin' load: (e, pageNum) -> $.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 Navigate.title() Index.board = "#{g.BOARD}" try if req.status is 200 Index.parse req.response, pageNum else if req.status is 304 and pageNum? Index.pageNav pageNum 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 timeEl = $ 'time#index-last-refresh', Index.navLinks timeEl.dataset.utc = Date.parse req.getResponseHeader 'Last-Modified' RelativeDates.update timeEl Index.scrollToIndex() parse: (pages, pageNum) -> Index.parseThreadList pages Index.buildThreads() Index.sort() if pageNum? Index.pageNav pageNum return Index.buildIndex() parseThreadList: (pages) -> Index.threadsNumPerPage = pages[0].threads.length 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 buildThreads: -> threads = [] posts = [] for threadData, i in Index.liveThreadData threadRoot = Build.thread g.BOARD, threadData if thread = g.BOARD.threads[threadData.no] thread.setPage i // Index.threadsNumPerPage 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 continue if thread.ID of thread.posts try posts.push new Post $('.opContainer', threadRoot), thread, g.BOARD 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 Main.callbackNodes Thread, threads Main.callbackNodes Post, posts Index.updateHideLabel() $.event 'IndexRefresh' buildHRs: (threadRoots) -> nodes = [] for node in threadRoots nodes.push node nodes.push $.el 'hr' nodes buildReplies: (threads) -> return unless Conf['Show Replies'] posts = [] for thread in threads 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] 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 thread.OP.nodes.root.parentNode, nodes Main.handleErrors errors if errors Main.callbackNodes Post, posts buildCatalogViews: -> catalogThreads = [] for thread in Index.sortedThreads when !thread.catalogView catalogThreads.push new CatalogThread Build.catalogThread(thread), thread Main.callbackNodes CatalogThread, catalogThreads Index.sortedThreads.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 = node.firstElementChild {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: -> switch Conf['Index Sort'] when 'bump' sortedThreadIDs = Index.liveThreadIDs when 'lastreply' sortedThreadIDs = [Index.liveThreadData...].sort (a, b) -> [..., a] = a.last_replies if 'last_replies' of a [..., b] = b.last_replies if 'last_replies' of b b.no - a.no .map (data) -> data.no when 'birth' sortedThreadIDs = [Index.liveThreadIDs...].sort (a, b) -> b - a when 'replycount' sortedThreadIDs = [Index.liveThreadData...].sort((a, b) -> b.replies - a.replies).map (data) -> data.no when 'filecount' sortedThreadIDs = [Index.liveThreadData...].sort((a, b) -> b.images - a.images).map (data) -> data.no Index.sortedThreads = sortedThreadIDs .map (threadID) -> g.BOARD.threads[threadID] .filter (thread) -> thread.isHidden is Index.showHiddenThreads if Index.isSearching Index.sortedThreads = Index.querySearch(Index.searchInput.value) or Index.sortedThreads # Sticky threads Index.sortOnTop (thread) -> thread.isSticky # Highlighted threads Index.sortOnTop (thread) -> thread.isOnTop or thread.isPinned sortOnTop: (match) -> offset = 0 for thread, i in Index.sortedThreads when match thread Index.sortedThreads.splice offset++, 0, Index.sortedThreads.splice(i, 1)[0] return buildIndex: (infinite) -> switch Conf['Index Mode'] when 'paged', 'infinite' pageNum = Index.getCurrentPage() if pageNum > Index.getMaxPageNum() # Go to the last available page if we were past the limit. Index.pageNav Index.getMaxPageNum() return threadsPerPage = Index.getThreadsNumPerPage() threads = Index.sortedThreads[threadsPerPage * pageNum ... threadsPerPage * (pageNum + 1)] nodes = threads.map (thread) -> thread.OP.nodes.root.parentNode Index.buildReplies threads nodes = Index.buildHRs nodes Index.buildPagelist() Index.setPage() when 'catalog' nodes = Index.buildCatalogViews() Index.sizeCatalogViews nodes else nodes = Index.sortedThreads.map (thread) -> thread.OP.nodes.root.parentNode Index.buildReplies Index.sortedThreads nodes = Index.buildHRs nodes $.rmAll Index.root unless infinite $.add Index.root, nodes $.event 'IndexBuild', nodes isSearching: false clearSearch: -> Index.searchInput.value = null Index.onSearchInput() Index.searchInput.focus() onSearchInput: -> if Index.isSearching = !!Index.searchInput.value.trim() unless Index.searchInput.dataset.searching Index.searchInput.dataset.searching = 1 Index.pageBeforeSearch = Index.getCurrentPage() pageNum = 0 else pageNum = Index.getCurrentPage() else return unless Index.searchInput.dataset.searching pageNum = Index.pageBeforeSearch delete Index.pageBeforeSearch <% if (type === 'userscript') { %> # XXX https://github.com/greasemonkey/greasemonkey/issues/1571 Index.searchInput.removeAttribute 'data-searching' <% } else { %> delete Index.searchInput.dataset.searching <% } %> Index.sort() if Conf['Index Mode'] in ['paged', 'infinite'] and Index.currentPage not in [pageNum, Index.getMaxPageNum()] # Go to the last available page if we were past the limit. Index.pageNav pageNum else Index.buildIndex() Index.setPage() querySearch: (query) -> return unless keywords = query.toLowerCase().match /\S+/g Index.search keywords search: (keywords) -> Index.sortedThreads.filter (thread) -> Index.searchMatch thread, 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