Infinite Scroll VOZ V2

try to take over the world!

当前为 2020-06-13 提交的版本,查看 最新版本

// infinite-voz.js
// ==UserScript==
// @name         Infinite Scroll VOZ V2
// @namespace    https://voz.vn
// @version      2.0
// @description  try to take over the world!
// @author       ReeganExE
// @match        https://voz.vn/t/*
// @grant        GM_addStyle
// ==/UserScript==
GM_addStyle(`
.hide {display: none} .show{display: block}
.fixed-page {
    position: fixed;
    left: 13px;
    bottom: 20px;
}

.reply-form.hide .message-cell--main {
  display: none;
}
.reply-form {
    position: fixed;
    bottom: 20px;
    z-index: 100;
    width: 0;
    border: solid 1px #616161;
    box-shadow: 1px 1px 10px 4px #585858;
    border-radius: 2px;
    -webkit-transition: width .2s ease-in-out;
    -moz-transition: width .2s ease-in-out;
    -o-transition: width .2s ease-in-out;
    transition: width .2s ease-in-out;
}

.reply-button {
  position: fixed;
  bottom: 38px;
  z-index: 100;
  right: 140px;
}
`)
;(function () {
  const PAGE_WRAPPER_SELECTOR = '.pageNavWrapper.pageNavWrapper--mixed'
  const POST_BODY_SELECTOR = '.lbContainer .block-body'
  const parser = new DOMParser()
  const threads = document.getElementById('posts')
  const posts = document.querySelector(POST_BODY_SELECTOR)
  let currentPage = +getCurrentPage()
  let lastPage = +getLastPage()
  let isLoading = false
  const PAGE_REG = /Page \d+/
  const BUFFER_HEIGHT = 300 // Magic number, to load next page before reach the end.
  const loadingSpinHTML =
    '<div class="" style="width: 160px;margin: 0 auto;padding: 20px;text-align: center;color: white;">Loading... <img src="data:image/gif;base64,R0lGODlhEAAQAPIAAP///wAAAMLCwkJCQgAAAGJiYoKCgpKSkiH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAADMwi63P4wyklrE2MIOggZnAdOmGYJRbExwroUmcG2LmDEwnHQLVsYOd2mBzkYDAdKa+dIAAAh+QQJCgAAACwAAAAAEAAQAAADNAi63P5OjCEgG4QMu7DmikRxQlFUYDEZIGBMRVsaqHwctXXf7WEYB4Ag1xjihkMZsiUkKhIAIfkECQoAAAAsAAAAABAAEAAAAzYIujIjK8pByJDMlFYvBoVjHA70GU7xSUJhmKtwHPAKzLO9HMaoKwJZ7Rf8AYPDDzKpZBqfvwQAIfkECQoAAAAsAAAAABAAEAAAAzMIumIlK8oyhpHsnFZfhYumCYUhDAQxRIdhHBGqRoKw0R8DYlJd8z0fMDgsGo/IpHI5TAAAIfkECQoAAAAsAAAAABAAEAAAAzIIunInK0rnZBTwGPNMgQwmdsNgXGJUlIWEuR5oWUIpz8pAEAMe6TwfwyYsGo/IpFKSAAAh+QQJCgAAACwAAAAAEAAQAAADMwi6IMKQORfjdOe82p4wGccc4CEuQradylesojEMBgsUc2G7sDX3lQGBMLAJibufbSlKAAAh+QQJCgAAACwAAAAAEAAQAAADMgi63P7wCRHZnFVdmgHu2nFwlWCI3WGc3TSWhUFGxTAUkGCbtgENBMJAEJsxgMLWzpEAACH5BAkKAAAALAAAAAAQABAAAAMyCLrc/jDKSatlQtScKdceCAjDII7HcQ4EMTCpyrCuUBjCYRgHVtqlAiB1YhiCnlsRkAAAOwAAAAAAAAAAAA=="/></div>'
  const loadingSpin = document.createElement('div')
  loadingSpin.innerHTML = loadingSpinHTML
  loadingSpin.className = 'hide'

  const pageNavWrappers = document.querySelectorAll(PAGE_WRAPPER_SELECTOR)
  pageNavWrappers[pageNavWrappers.length - 1].classList.add('fixed-page')

  // Reply form
  const repyForm = document.querySelector('form.js-quickReply')
  if (repyForm) {
    repyForm.classList.add('reply-form')
    repyForm.classList.add('hide')

    // Reply button
    const replyButton = htmlToElement(`
      <button type="button" class="button--primary button button--icon button--icon--reply reply-button">
        <span class="button-text">
          Reply
        </span>
      </button>
    `)

    let show = false

    replyButton.addEventListener('click', () => {
      const post = document.querySelector('.message.message--post.js-post.js-inlineModContainer')
      show = !show
      if (show) {
        repyForm.classList.remove('hide')
        repyForm.style.width = `${post.clientWidth}px`
      } else {
        repyForm.addEventListener(
          'transitionend',
          () => {
            repyForm.classList.add('hide')
          },
          { once: true }
        )
        repyForm.style.width = 0
      }
    })
    document.body.appendChild(replyButton)
  }

  //= =========================================================================
  // Load Thread
  // UNSUPPORTED AT THIS MOMENT
  //= =========================================================================
  if (threads) {
    const boxId = getParameterByName('f', window.location.href)
    const innerThreadList = document.getElementById(`threadbits_forum_${boxId}`)
    const threadListOffsetTop = getCoords(threads).top

    insertAfter(threads, loadingSpin)
    window.addEventListener('scroll', function () {
      if (
        window.scrollY + window.innerHeight + BUFFER_HEIGHT >=
        threads.offsetHeight + threadListOffsetTop
      ) {
        if (isLoadable()) {
          loadingSpin.className = 'show'
          isLoading = true
          loadBoxPage(boxId, ++currentPage, function (loadedDoc) {
            pushState(currentPage)
            innerThreadList.innerHTML += `<div>Page${currentPage}</div>`
            innerThreadList.innerHTML += loadedDoc.getElementById(
              `threadbits_forum_${boxId}`
            ).innerHTML
            lastPage = getLastPage(loadedDoc)
            updatePageNavigator(loadedDoc.querySelector('div.pagenav').innerHTML)
            isLoading = false
            loadingSpin.className = 'hide'
          })
        }
      }
    })
    //= =========================================================================
    // Load Post
    //= =========================================================================
  } else if (posts) {
    const postsOffsetTop = getCoords(posts).top

    const [, threadId] = location.href.match(/https:\/\/voz.vn\/t.*\.(\d+)\/?/)
    insertAfter(posts, loadingSpin)
    window.addEventListener('scroll', function () {
      if (
        window.scrollY + window.innerHeight + BUFFER_HEIGHT >=
        posts.offsetHeight + postsOffsetTop
      ) {
        if (isLoadable()) {
          loadingSpin.className = 'show'
          isLoading = true
          loadThreadPage(threadId, ++currentPage, function (loadedDoc) {
            pushState(currentPage)
            posts.innerHTML += loadedDoc.querySelector(POST_BODY_SELECTOR).innerHTML
            updatePageNavigator(loadedDoc)
            lastPage = getLastPage(loadedDoc)
            isLoading = false
            loadingSpin.className = 'hide'
          })
        }
      }
    })
  }

  function pushState(currentPage) {
    let { title } = document

    if (PAGE_REG.test(title)) {
      title = title.replace(PAGE_REG, `Page ${currentPage}`)
    } else {
      title = `${title} Page ${currentPage}`
    }

    let [root] = location.href.split('/page-')
    if (!root.endsWith('/')) {
      root += '/'
    }
    root += `page-${currentPage}`
    history.pushState({}, title, root + location.search)
    document.title = title
  }

  function isLoadable() {
    return !isLoading && currentPage < lastPage
  }

  function getCurrentPage() {
    return document.querySelector('.pageNav-page.pageNav-page--current a').textContent.trim()
  }

  function updatePageNavigator(loadedDoc) {
    const pageNavs = document.querySelectorAll(PAGE_WRAPPER_SELECTOR)
    const newHtmlNav = loadedDoc.querySelector(PAGE_WRAPPER_SELECTOR).innerHTML
    for (let i = 0; i < pageNavs.length; i++) {
      pageNavs[i].innerHTML = newHtmlNav
    }
  }

  function getLastPage(doc) {
    if (!doc) doc = document
    return document.querySelector('.pageNav-main li:last-child').innerText
  }

  function getParameterByName(name, url) {
    if (!url) {
      url = window.location.href
    }
    name = name.replace(/[[\]]/g, '\\$&')
    const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`)
    const results = regex.exec(url)
    if (!results) return null
    if (!results[2]) return ''
    return decodeURIComponent(results[2].replace(/\+/g, ' '))
  }

  function getCoords(elem) {
    // crossbrowser version
    const box = elem.getBoundingClientRect()

    const { body } = document
    const docEl = document.documentElement

    const scrollTop = window.pageYOffset || docEl.scrollTop || body.scrollTop
    const scrollLeft = window.pageXOffset || docEl.scrollLeft || body.scrollLeft

    const clientTop = docEl.clientTop || body.clientTop || 0
    const clientLeft = docEl.clientLeft || body.clientLeft || 0

    const top = box.top + scrollTop - clientTop
    const left = box.left + scrollLeft - clientLeft

    return { top: Math.round(top), left: Math.round(left) }
  }

  function loadBoxPage(boxId, pageNo, callback) {
    ajax(
      'GET',
      `https://vozforums.com/forumdisplay.php?f=${boxId}&order=desc&page=${pageNo}`,
      loadSuccess
    )
    function loadSuccess(xhr) {
      callback(parser.parseFromString(xhr.responseText, 'text/html'))
    }
  }

  function loadThreadPage(threadId, pageNo, callback) {
    ajax('GET', `https://voz.vn/t/thread.${threadId}/page-${pageNo}`, (xhr) =>
      callback(parser.parseFromString(xhr.responseText, 'text/html'))
    )
  }

  function ajax(method, url, callback) {
    const xhr = new XMLHttpRequest()
    xhr.open(method, url)
    xhr.send(null)
    xhr.onreadystatechange = function () {
      const DONE = 4 // readyState 4 means the request is done.
      const OK = 200 // status 200 is a successful return.
      if (xhr.readyState === DONE) {
        if (xhr.status === OK) {
          callback(xhr)
        } else {
          console.log(`Error: ${xhr.status}`) // An error occurred during the request.
        }
      }
    }
  }

  function insertAfter(referenceNode, newNode) {
    referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling)
  }

  /**
   * @param {String} HTML representing a single element
   * @return {Element}
   * @author https://stackoverflow.com/a/35385518/1099314
   */
  function htmlToElement(html) {
    const template = document.createElement('template')
    html = html.trim() // Never return a text node of whitespace as the result
    template.innerHTML = html
    return template.content.firstChild
  }
})()