The cause of

A few days ago the company asked to write an anchor auto-highlight feature, where the page scroll to the anchor highlight.

So here is a simple record, because of the automatic highlighting decision problem, so sent here, if there is a better method, I hope to teach me ~ ~

rendering

Click here to see it in action

Analyze the implementation steps

This is the rough DOM structure

<html> 
  <body>
    <ul>
      <li><a href="#a1">a1</a></li>
      <li><a href="#a2">a2</a></li>
      <li><a href="#a3">a3</a></li>
      <li><a href="#a4">a4</a></li>
      <li><a href="#a5">a5</a></li>
    </ul>
    <div id="container">
      <div id="a1"></div>
      <div id="a2"></div>
      <div id="a3"></div>
      <div id="a4"></div>
      <div id="a5"></div>
    </div>
  </body>
</html>
Copy the code

Elements to be processed

  • The page needs to deal withaThe label is in the diagramanchor
  • The location of the anchor points is the various bandsidElement, as shown in figurea1 element 和 a2 element
  • Wraps around the locations of these anchor pointsThe containerIf it’s the entire pagedocument, or a custom rolling container, as we call it herecontainer

How do I manually highlight the A label

To highlight, simply add a class to the corresponding A tag

// Get all elements of a
function getAllAnchorElement(container) {
    const target = container ? container : document
    return target.querySelectorAll('a')}// Add the highlighted class name corresponding to the ID. Remove the highlighted class name that is not corresponding to the ID
function highLightAnchor(id){
  getAllAnchorElement().forEach(element= > {
    element.classList.remove('highLight')
    if (element.hash.slice(1) == id) {
      element.classList.add('highLight')}}); }Copy the code

How do I automatically highlight the A label

The principle is very simple, is to listen to the container element scroll, under the corresponding conditions automatically highlight the A label

// Use thorttle to handle handleScroll, otherwise the page may be stuck ðŸĪŠðŸĪŠ
const throttleFn = throttle(handleScroll, 100)

// If you do not prevent rolling events, add passive: true to increase rolling performance
ScrollContrainer.addEventListener('scroll', throttleFn, {passive: true})
Copy the code

But theCorresponding conditionsA little trouble, I think of three methods, each has its own disadvantages and advantages, if you have a perfect method, please kindly advise

My solution here is to use getBoundingClientRect to determine the location of the element.

Here is a bit of explanation from MDN

The return value is a DOMRect object, which is a collection of rectangles returned by the element’s getClientRects() method, that is, the collection of CSS borders associated with the element.

The DOMRect object contains a set of read-only attributes — left, top, Right, and bottom — that describe the border, in pixels. All attributes except width and height are relative to the top-left position of the viewport.

Some friends suggested that IntersectionObserver API would be better! I used this scheme from the very beginning, but he couldn’t deal with one problem. In the following figure, after a3 and A4 appear completely, their intersectionRatio will always be 1, and the entries array will not contain A3 and A4, which will lead to inaccurate proportion of some schemes. So I did not use, if I use wrong, please tell me thank you thank you

I have four options here, but let’s start with the first one

Plan 1: Highlight whoever pops up

let highligthId;// A highlighted ID is required
const windowHeight = this.ScrollContrainer.offsetHeight // Container height
this.anchors.forEach(element= > {
  const id = element.hash.slice(1)
  const target = document.getElementById(id)
  if (target) {
    const {
      top
    } = target.getBoundingClientRect()
    // When the element header is visible
    if (top < windowHeight) {
      highligthId = id
    }
  }
})
if (highligthId) {
  // Call the highlight method
  this.highLightAnchor(highligthId)
}
Copy the code

advantages

  • simple

disadvantages

  • Initial state ifThe first elementI don’t have a full screen likea1“A little bit at the bottom of the screena2Elements, thea1Element highlighting will jump away from the inside at this timea1I can’t highlight it anymore

Plan 2: Highlight whoever occupies the largest percentage of the screen

let highligthId;
let maxRatio = 0 // The percentage of the screen occupied
const windowHeight = this.ScrollContrainer.offsetHeight
this.anchors.forEach(element= > {
  const id = element.hash.slice(1)
  const target = document.getElementById(id)
  if (target) {
    let visibleRatio = 0;
    let {
      top,
      height,
      bottom
    } = target.getBoundingClientRect();
    // When all elements are visible
    if (top >= 0 && bottom <= windowHeight) {
      visibleRatio = height / windowHeight
    }
    // When the element is visible in the header
    if (top >= 0 && top < windowHeight && bottom > windowHeight) {
      visibleRatio = (windowHeight - top) / windowHeight
    }
    // When elements fill the screen
    if (top < 0 && bottom > windowHeight) {
      visibleRatio = 1
    }
    // When the end of the element is visible
    if (top < 0 && bottom > 0 && bottom < windowHeight) {
      visibleRatio = bottom / windowHeight
    }
    if(visibleRatio >= maxRatio) { maxRatio = visibleRatio; highligthId = id; }}});if (highligthId) {
  this.highLightAnchor(highligthId)
}
Copy the code

advantages

  • This works well when each element is larger than half the screen

disadvantages

  • At the top of the page ifa2The proportion of thana1Great, thata1I can’t highlight it
  • At the bottom of the pagea5 æŊ” a4A small,a5I can’t highlight it becausea5It’s a huge percentage of the screena4 įš„

Plan 3: Highlight whoever has the largest percentage of themselves displayed

The code makes the same judgment as highlighting whoever occupies a larger percentage of the screen, except that the denominator is different

let highligthId;
let maxRatio = 0
const windowHeight = this.ScrollContrainer.offsetHeight
this.anchors.forEach(element= > {
  const id = element.hash.slice(1)
  const target = document.getElementById(id)
  if (target) {
    let visibleRatio = 0;
    let {
      top,
      height,
      bottom
    } = target.getBoundingClientRect();
    // When all elements are visible
    if (top >= 0 && bottom <= windowHeight) {
      visibleRatio = 1
    }
    // When the element is visible in the header
    if (top >= 0 && top < windowHeight && bottom > windowHeight) {
      visibleRatio = (windowHeight - top) / height
    }
    // When elements fill the screen
    if (top < 0 && bottom > windowHeight) {
      visibleRatio = windowHeight / height
    }
    // When the end of the element is visible
    if (top < 0 && bottom > 0 && bottom < windowHeight) {
      visibleRatio = bottom / height
    }
    if(visibleRatio >= maxRatio) { maxRatio = visibleRatio; highligthId = id; }}});if (highligthId) {
  this.highLightAnchor(highligthId)
}
Copy the code

advantages

  • This works well when each element is larger than half the screen
  • When there are small elements in succession, the effect will be lessA scheme to highlight whoever occupies the largest proportion of the screenBetter

disadvantages

  • If both a1 and A2 are displayed at the top of the page, a1 is not as high priority as A2 and cannot be highlighted

  • In the middle of the page a3 is very small, so every time a3 appears, the calculation ratio is 1. A4 must wait for A3 to disappear before it can be highlighted

(new)Plan 4: Highlight whoever is near the top

type4(e, offsetTop = 0) {
  let highligthId = Array.prototype.reduce.call(this.anchors, (prev, curr) => {
    const id = curr.hash.slice(1)
    const target = document.getElementById(id)
    if (target) {
      const {
        top
      } = target.getBoundingClientRect()
      // Top <= offsetTop when the header distance from the top is less than the specified range
      return top <= offsetTop && top > prev.top ? {
        id,
        top
      } : prev
    } else {
      return prev
    }
  }, {
    id: null.top: -Infinity
  }).id;
  if (highligthId) {
    this.highLightAnchor(highligthId)
  }
}
Copy the code

advantages

  • Easy, at the bottom of the pageExtra elements support the sceneWorks best when the last element itself is a screen high

disadvantages

  • inNo extra elements to support the sceneWhen, the bottom elementa5Unable to highlight

The last

As you can see from the above, every solution I can think of has problems in some cases.

In my work, because every element is at least half the screen size, highlighting the one that occupies the largest percentage of the screen is the best combination.

In fact, in the beginning, we used the scheme to highlight whoever has a large percentage of themselves, but this scheme is not effective when one element is very, very long.

If the above content is helpful to you, may I cheat a thumbs-up 👍👍👍👍

The last of the last to attach all the code


      
<html>

<head>
  <meta charset="UTF-8">
</head>

<body>
  <nav>
    <ul>
      <li><a href="#a1">a1</a></li>
      <li><a href="#a2">a2</a></li>
      <li><a href="#a3">a3</a></li>
      <li><a href="#a4">a4</a></li>
      <li><a href="#a5">a5</a></li>
    </ul>
    <input type="radio"
           name="strategy"
           checked
           value="type1">Highlight your head<br><br>
    <input type="radio"
           name="strategy"
           value="type2">Highlight as much of the screen as possible<br><br>
    <input type="radio"
           name="strategy"
           value="type3">A larger percentage of itself is highlighted<br><br>
    <input type="radio"
           name="strategy"
           value="type4">Highlight near the top<br><br>
    <input type="checkbox"
           name="bug"
           value="abnormality">The size of the element that triggered the bug<br><br>
    <input type="checkbox"
           name="friendly_link"
           value="friendly_link">Add a friendship element<br><br>
    <p>The lower right corner of the color can be dragged to change the size of the corresponding element</p>
  </nav>

  <div id="container">
    <div id="a1"></div>
    <div id="a2"></div>
    <div id="a3"></div>
    <div id="a4"></div>
    <div id="a5"></div>
    <div id="friendly_link"></div>
  </div>
</body>
<script>
  function throttle(fn, interval = 1000) {
    let timer = null;
    return function(. args) {
      if(! timer) { timer = setTimeout((a)= > {
          timer = null
          fn.call(this. args) }, interval); }}}class AutoHighLightAnchor {
    // anchors;
    // ScrollContrainer;
    // throttleFn;
    // strategy;
    constructor(anchorsContainer, ScrollContrainer, strategy = AutoHighLightAnchor.Strategys.type1) {
      this.anchors = anchorsContainer.querySelectorAll('a')
      this.ScrollContrainer = ScrollContrainer;
      this.strategy = strategy;
      this.init()
    }

    init(strategy = this.strategy) {
      if (this.throttleFn) {
        this.remove()
      }
      this.throttleFn = throttle(this[strategy].bind(this), 100)
      this.throttleFn() // Perform an initial update position
      this.ScrollContrainer.addEventListener('scroll'.this.throttleFn, {
        passive: true
      })
    }
    remove() {
      this.ScrollContrainer.removeEventListener('scroll'.this.throttleFn, {
        passive: true
      })
    }

    highLightAnchor(id) {
      this.anchors.forEach(element= > {
        element.classList.remove('highLight')
        if (element.hash.slice(1) == id) {
          element.classList.add('highLight')}}); } type1(e) {let highligthId;
      const windowHeight = this.ScrollContrainer.offsetHeight
      this.anchors.forEach(element= > {
        const id = element.hash.slice(1)
        const target = document.getElementById(id)
        if (target) {
          const {
            top
          } = target.getBoundingClientRect()
          // When the element header is visible
          if (top < windowHeight) {
            highligthId = id
          }
        }
      })
      if (highligthId) {
        this.highLightAnchor(highligthId)
      }
    }

    type2(e) {
      let highligthId;
      let maxRatio = 0
      const windowHeight = this.ScrollContrainer.offsetHeight
      this.anchors.forEach(element= > {
        const id = element.hash.slice(1)
        const target = document.getElementById(id)
        if (target) {
          let visibleRatio = 0;
          let {
            top,
            height,
            bottom
          } = target.getBoundingClientRect();
          // When all elements are visible
          if (top >= 0 && bottom <= windowHeight) {
            visibleRatio = height / windowHeight
          }
          // When the element is visible in the header
          if (top >= 0 && top < windowHeight && bottom > windowHeight) {
            visibleRatio = (windowHeight - top) / windowHeight
          }
          // When elements fill the screen
          if (top < 0 && bottom > windowHeight) {
            visibleRatio = 1
          }
          // When the end of the element is visible
          if (top < 0 && bottom > 0 && bottom < windowHeight) {
            visibleRatio = bottom / windowHeight
          }
          if(visibleRatio >= maxRatio) { maxRatio = visibleRatio; highligthId = id; }}});if (highligthId) {
        this.highLightAnchor(highligthId)
      }
    }

    type3(e) {
      let highligthId;
      let maxRatio = 0
      const windowHeight = this.ScrollContrainer.offsetHeight
      this.anchors.forEach(element= > {
        const id = element.hash.slice(1)
        const target = document.getElementById(id)
        if (target) {
          let visibleRatio = 0;
          let {
            top,
            height,
            bottom
          } = target.getBoundingClientRect();
          // When all elements are visible
          if (top >= 0 && bottom <= windowHeight) {
            visibleRatio = 1
          }
          // When the element is visible in the header
          if (top >= 0 && top < windowHeight && bottom > windowHeight) {
            visibleRatio = (windowHeight - top) / height
          }
          // When elements fill the screen
          if (top < 0 && bottom > windowHeight) {
            visibleRatio = windowHeight / height
          }
          // When the end of the element is visible
          if (top < 0 && bottom > 0 && bottom < windowHeight) {
            visibleRatio = bottom / height
          }
          if(visibleRatio >= maxRatio) { maxRatio = visibleRatio; highligthId = id; }}});if (highligthId) {
        this.highLightAnchor(highligthId)
      }
    }

    type4(e, offsetTop = 0) {
      let highligthId = Array.prototype.reduce.call(this.anchors, (prev, curr) => {
        const id = curr.hash.slice(1)
        const target = document.getElementById(id)
        if (target) {
          const {
            top
          } = target.getBoundingClientRect()
          // Top <= offsetTop when the header distance from the top is less than the specified range
          return top <= offsetTop && top > prev.top ? {
            id,
            top
          } : prev
        } else {
          return prev
        }
      }, {
        id: null.top: -Infinity
      }).id;
      if (highligthId) {
        this.highLightAnchor(highligthId)
      }
    }
  }
  AutoHighLightAnchor.Strategys = {
    type1: 'type1'.type2: 'type2'.type3: 'type3'.type4: 'type4'
  }

  const high = new AutoHighLightAnchor(document.querySelector('ul'), document.querySelector('#container'),
    AutoHighLightAnchor.Strategys.type1)

  document.querySelectorAll('input[type=radio]').forEach(element= > {
    element.onchange = e= > high.init(e.target.value)
  })
  document.querySelector('input[name=bug]').onchange = e= > {
    const value = e.target.checked
    const elements = document.querySelectorAll('#container>div')
    if (value) {
      const abnormality = [30.120.20.30.50]
      elements.forEach((element, index) = > {
        element.style.height = abnormality[index] + 'vh'})}else {
      elements.forEach((element, index) = > {
        element.style.height = (100 - 10 * index) + 'vh'}}})document.querySelector('input[name=friendly_link]').onchange = e= > {
    const value = e.target.checked
    const element = document.querySelector('#friendly_link')
    if (value) {
      element.style.display = 'block'
    } else {
      element.style.display = 'none'}}</script>
<style>
  body {
    margin: 0;
  }

  a {
    display: block;
    width: 100%;
    height: 100%;
    color: #898A95;
    line-height: 50px;
    font-size: 40px;
    border-radius: 0 50px 50px 0;
    text-decoration: none;
    text-align: center;
  }

  .highLight {
    color: #ffffff;
    background: #1b3781;
  }

  nav {
    width: 250px;
    float: left;
    height: 100vh;
    overflow: scroll;
  }

  ul {
    width: 220px;
    display: flex;
    flex-direction: column;
    margin: 0;
    padding: 0;
    list-style: none;
  }

  #container {
    height: 100vh;
    overflow: scroll;
  }

  #container>div {
    position: relative;
    resize: vertical;
    overflow: scroll;
    /* color: #ffffff; font-size: 30px; * /
  }

  #container>div::after {
    content: attr(id);
    display: block;
    font-size: 100px;
    color: #fff;
    text-align: center;
    width: 100%;
    position: absolute;
    top: 50%;
    transform: translateY(50%); }#container>div:hover {
    outline: 1px dashed #09f;
  }

  #container>div::-webkit-scrollbar {
    width: 25px;
    height: 20px;
  }

  #a1 {
    height: 90vh;
    background-color: #77b9e1;
  }


  #a2 {
    height: 90vh;
    background-color: #9fc6e6;
  }

  #a3 {
    height: 90vh;
    background-color: #73a5d7;
  }

  #a4 {
    height: 90vh;
    background-color: #1387aa;
  }

  #a5 {
    height: 90vh;
    background-color: #0c5ea8;
  }

  #friendly_link {
    display: none;
    height: 90vh;
    background-color: #7e92d3;
  }
</style>

</html>
Copy the code