hewenguang / circle

让网页赏心悦目、让阅读回归初心。Circle 阅读模式提供更隐私、更轻松、更舒适的网页阅读体验
http://circlereader.com/
MIT License
468 stars 56 forks source link

Feature Request: Keyboard driven navigation #13

Open snowman opened 3 years ago

snowman commented 3 years ago

Feel free to close this feature if you think not worth it.

Usage:

Open page: https://ranhe.xyz/my-career/

Preparation:

  1. Open Console, run the following javascript
  2. Press 1 to select block element such as paragraph, div, etc as first element
  3. Press 2 to select block element such as paragraph, div, etc as second element

    Details: Once two elements are selected, the common parent will be calculated, and mark the first element as starting point, you can repeat 1 or 2 to reset the element or re-calculate the common parent.

Shortcuts: (j / k is the Vim default keybinding for moving cursor down/up)

  1. Use j to navigate to the next sibling
  2. Use k to navigate to the previous sibling

How do you feel? Hope this supports in Circle, the above "Preparation" section should not be needed when in read mode, because we can easily mark the first paragraph as the starting point.

The script is written by me, feel free to do whatever you want. The code is a mess, just gives you inspiration for what may need to improve. Currently, the code does not support navigate list items one by one.

Pros:

  1. Don't need to move your eyes anymore, which means never lose yourself while reading
  2. Easily skip the long image
  3. Scroll in the middle of a line of text and scroll back a little bit manually never happens
  4. Without using mouse to keep scrolling, and just one keystroke to go, scroll by element instead of pixels
// ==UserScript==
// @name        Navigation System
// @description Navgiate page easily
// @author      snowman
// @match       <all_urls>
// @require     http://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js
// @version     1.0
// @grant       GM_addStyle
// @grant       GM.getValue
// ==/UserScript==

"use strict"

function off() {
  function get_page_height() {
    // https://stackoverflow.com/questions/1145850/how-to-get-height-of-entire-document-with-javascript
    let body = document.body,
      html = document.documentElement

    let height = Math.max(
      body.scrollHeight,
      body.offsetHeight,
      html.clientHeight,
      html.scrollHeight,
      html.offsetHeight
    )

    return height
  }

  const VIEWPORT_HEIGHT = window.innerHeight
  const PAGE_HEIGHT = get_page_height()

  return { VIEWPORT_HEIGHT, PAGE_HEIGHT }
}

let first, second
let status
let located
let active

let scroll_to_bottom = $e => scroll_to($e, true)
let scroll_to_top = $e => scroll_to($e, false)

let getOffset = top => {
  let { PAGE_HEIGHT, VIEWPORT_HEIGHT } = off()
  let indicator = 0

  return Math.max(0, Math.min(PAGE_HEIGHT - VIEWPORT_HEIGHT, top - indicator))
}

let scroll_to = ($e, bottom = false) => {
  let height_of_element_include_margin_border = e =>
    $(e).outerHeight(true) - parseInt($(e).css("marginTop"))

  let offset =
    $e.offset().top +
    (bottom
      ? height_of_element_include_margin_border($e)
      : -parseInt($e.css("marginTop")))

  console.log(`move to offset:`, offset)

  let y = getOffset(offset)

  _scrollTo(y)
}

function _scrollTo(y) {
  window.scrollTo({
    top: y,
    behavior: "smooth"
  })
}

let next = () => {
  let $navi_current = $(`.navi-current`)
  let $navi_next = $navi_current.nextAll(":visible").slice(0, 1)

  // if ($navi_current.length == 0) {
  //   $topics.first().addClass("navi-current")
  //   scroll_to_bottom($(`.navi-current`))
  // }

  if ($navi_next.length) {
    let true_top =
      $navi_current.offset().top - parseInt($navi_current.css("marginTop"))

    console.log("true_top", true_top)
    console.log("window.pageYOffset", window.pageYOffset)

    // https://stackoverflow.com/questions/4096863/how-to-get-and-set-the-current-web-page-scroll-position
    if (Math.abs(true_top - window.pageYOffset) > 48) {
      console.log(`scrolling to position where current element is at top...`)

      scroll_to_top($navi_current)

      return
    }

    scroll_to_bottom($navi_current)
    $navi_current.removeClass("navi-current")
    $navi_next.addClass("navi-current")
  }
}

let prev = () => {
  let $navi_current = $(`.navi-current`)
  let $navi_prev = $navi_current.prevAll(":visible").slice(0, 1)

  if ($navi_prev.length) {
    $navi_current.removeClass("navi-current")
    $navi_prev.addClass("navi-current")
    scroll_to_top($navi_prev)
  }
}

;(function() {
  jQuery(document).ready($ => {
    document.$ = $

    const internalCSS = styles =>
      $(`<style type="text/css">${styles}</style>`).appendTo("head")

    const styles = `
      *:hover {
         outline: solid 5px rgba(255, 0, 0, 0.5) !important;
      }
`

    // if you set class with "border-left", it will reflow text of element
    // results in different height, and scroll to wrong position.
    //
    // and sadly, there is not style called "outline-left"
    //
    // so use box-shadow instead:
    //   https://stackoverflow.com/questions/43729480/outline-to-only-one-side-of-div
    const internalStyles = `
.navi-current {
  box-shadow: inset 2px 0px 0px 0px red;
background-color: let(--topic-item-hover-background-color);
}
`

    internalCSS(internalStyles)

    function get_common_parent_longest(e1, e2) {
      return $(e1)
        .parents()
        .has(e2)
        .first()
    }

    function get_direct_children_of_common_parent(e1, e2) {
      e1 = $(e1)
      e2 = $(e2)

      let common = get_common_parent_longest(e1, e2)[0]

      let chain = e1
        .parents()
        .add(e1)
        .toArray()

      for (let idx = 0; idx < chain.length; idx++) {
        const element = chain[idx]

        if (element == common) {
          located = chain[idx + 1]
          return located
        }
      }
    }

    let ctre = {
      mouseover: function(e) {
        if (ctre.hoveredElement != e.target) {
          ctre.hoveredElement = e.target
          ctre.highlightElement()
        }
      },
      addHighlightStyle: function(elm) {
        ctre.markedElement.style.setProperty(
          "outline",
          "solid 5px rgba(255,0,0,0.5)",
          "important"
        )
        ctre.markedElement.style.setProperty(
          "outline-offset",
          "-5px",
          "important"
        )
      },
      highlightElement: function() {
        if (!ctre.hoveredElement) return

        if (ctre.markedElement) {
          ctre.removeHighlightStyle(ctre.markedElement)
        }

        ctre.markedElement = ctre.hoveredElement

        ctre.addHighlightStyle(ctre.markedElement)
      },
      removeHighlightStyle: function(elm) {
        ctre.markedElement.style.outline = ""
        ctre.markedElement.style.outlineOffset = ""
      },
      keyDown: function(e) {
        if (e.keyCode == 27) {
          // esc
          ctre.off()
        }

        if (e.keyCode == 68) {
          // d
          ctre.init()
        }

        if (e.keyCode == 49) {
          // 1
          status = "KEY_1"

          ctre.init()
        }

        if (e.keyCode == 50) {
          // 2
          status = "KEY_2"

          ctre.init()
        }

        if (e.keyCode == 74) {
          // j
          next()
        }

        if (e.keyCode == 75) {
          // k
          prev()
        }
      },
      init: function(e) {
        $("body").on("mouseover", ctre.mouseover)
      },
      mousedown: function(e) {
        if (status == "KEY_1") {
          first = e.target
          console.log(`set first to`, e.target)
        }
        if (status == "KEY_2") {
          second = e.target
          console.log(`set second to`, e.target)
        }

        if (status && first && second) {
          let c = get_common_parent_longest(first, second)
          console.log(`set common parent to`, c)

          active = get_direct_children_of_common_parent(first, second)
          console.log(`active:`, active)

          $(".navi-current").removeClass("navi-current")
          $(active).addClass("navi-current")
        }

        if (status) {
          status = null

          ctre.off()

          e.preventDefault()
          e.stopPropagation()

          return false
        }
      },
      off: function(e) {
        ctre.removeHighlightStyle()
        $("body").off("mouseover", ctre.mouseover)
      }
    }

    $("body").on("keydown", ctre.keyDown)
    $("body").on("click", ctre.mousedown)
  })
})()
hewenguang commented 3 years ago

收到了,我研究研究

我是维护者,同时我是中国人,而且我英文不是很好,提 bug 还是中文吧,😂