background

Recently, I have been engaged in the development of mobile terminal Web applications. With the increasing number of product iteration pages and the increase of a page module, the product manager proposed the requirement of adding anchor points to pages with more content modules.

There are a lot of products on the market that should have ready-made components that can be used, but after some painstaking research (SOU) (SUO), no suitable one was found. Since there is no ready-made, then write a bai, anyway page module anchor function also is not very complex, can spend much time, result oneself write a page of anchor test problems, on the next page found a function is not applicable, write and write again alone, and all sorts of problems, a small anchor function of pit, Have to feel there should be a good design before you start.

Design goals

  • Improve the online product experience, so that users click on the anchor point interaction more smoothly, as smooth as silk

  • Reduce repetitive development labor costs, achieve a set of anchor components multi-page reuse

Problem of foresight

The composition of the page module anchor function

  • Anchor navigation bar

  • Anchor content presentation module

Similar effects are shown below:

Page module anchor point needs to implement functions

  • Anchor navigation click the scroll wheel to slide the corresponding content module

  • The corresponding anchor navigation highlight is displayed in the content module view

  • Navigation bar starts to hide when sucking top

Realization of core functions

Anchor navigation click the scroll wheel to slide the corresponding content module

Implementation method

1, a tag href=#id implementation of the corresponding content module anchor point. The disadvantages are that links change, the top of the content module is aligned with the top of the view – uncontrollable, no scrollbar scrolling events are triggered

2. Use the Element interface’s scrollIntoView() method to scroll the content module into the view. The disadvantage is that only the top of the content module is aligned with the top of the view and the bottom of the content module is aligned with the bottom of the view – no precise distance control can be achieved

const contentDom = document.querySelector(`#content${index}`)
contentDom.scrollIntoView({
  behavior: 'smooth',
  block: 'start',
  inline: 'nearest'
})
Copy the code

Use requestAnimationFrame to change the animate top of a scrollTop element. The disadvantages are the need to artificially implement complex animation effects, consideration of boundaries, and data processing.

const contentDom = document.querySelector(`#content${index}`) const scrollDom = document.querySelector(`.layout-main`) Const {TopRange} = this.state if (this.animateTotop) return const animateStep = () => { Let allRange = contentDom. GetBoundingClientRect (). The top - TopRange + 1 / / when times rolling distance let nowRange = allRange / 10 / / surplus to scroll Let nextRange = allRange - nowRange if (math.abs (nextRange) > 10) {// if the remaining distance to be rolled is greater than 10 let oldScrollTop = Scrolldom. scrollTop += nowRange // Trigger the scroll wheel // If the scroll bar sends the change - execute the next animation if (oldScrollTop! . = = scrollDom scrollTop) {window. RequestAnimationFrame (animateStep)} else {/ / slide to complete this. AnimateToTop = false}} the else {// If the remaining distance to scroll is less than 10 scrolldom. scrollTop += nowRange + nextRange // trigger the scroll wheel // slide done this.animateToTop = false}} this.animateToTop = true animateStep()Copy the code

The corresponding anchor navigation highlight is displayed in the content module view

Implementation method

1. Listen for the scroll element scroll event. Traverse the content module, judge the first element from the top of the view – height is greater than (retained height | | 0)

document.querySelector(`.layout-main`).addEventListener('scroll', (e) => {const {TopRange} = this.state for (let I = 0; i < ModuleList.length; I ++) {const contentDom = document.querySelector(' #content${I} ' ContentDom. GetBoundingClientRect (). The top / / boundary value - 1 prevent inaccurate if click anchor transform ((domTop - 1) > TopRange) {/ / current boundary element as a const content module pointSelectIndex = i > 1 ? i - 1 : 0 pointSelectIndex ! == this.state.pointSelectIndex && this.setState({ pointSelectIndex }) break } } })Copy the code

Use requestAnimationFrame to determine the current anchor subscript in real time

Const step = () => {const {TopRange} = this.state i < ModuleList.length; I ++) {const contentDom = document.querySelector(' #content${I} ' ContentDom. GetBoundingClientRect (). The top / / boundary value - 1 prevent inaccurate if click anchor transform ((domTop - 1) > TopRange) {/ / current boundary element as a const content module pointSelectIndex = i > 1 ? i - 1 : 0 pointSelectIndex ! == this.state.pointSelectIndex && this.setState({ pointSelectIndex }) break } } this.requestAnimationFrameInstance = Window. RequestAnimationFrame (step) / / store instance} step ()Copy the code

3. IntersectionObserver is used to observe the content module asynchronously and calculate the current highlight anchor point according to the display and position of the content module

Let {TopRange} = this.state // Store height // viewer options let options = {root: document.querySelector('.layout-main'), threshold: [0, 1], rootMargin: ` - ${TopRange} px 0 px 0 px 0 px `} / / create a viewer enclosing ioObserver = new Windows. IntersectionObserver (function (entries) { Entries.reverse ().foreach (function (entry) {// The top margin of the element in the observation area && is negative if (entry.isintersecting && Entry. BoundingClientRect. Top < TopRange) {entry. The target. The active ()}})}, options) / / traverse content module, Modulelist.foreach ((item, index) => { let element = document.querySelector(`#content${index}`) element.active = () => { index ! == this.state.pointSelectIndex && this.setState( { pointSelectIndex: This.ioobserver.observe (element)Copy the code

Navigation bar starts to hide when sucking top

Implementation method

1. Listen for the scroll element scroll event. Start topping when anchor navigation is about to disappear into view and stop topping when the first content module element is about to be fully displayed in view.

document.querySelector(`.layout-main`).addEventListener('scroll', Const navDom = document.querySelector(' #NAV_ANCHOR ') // (e) => {let {TopRange} = this.state // Hold height // judge anchor navigation start top The current element from the top of the view distance let navDomTop = navDom. GetBoundingClientRect (). The top / / whether the current navigation elements will not in view the if (navDomTop < = {TopRange)! this.state.isNavFixed && this.setState({ isNavFixed: Const firstDom = document.querySelector(' #content0 ') // Let firstDomTop = FirstDom. GetBoundingClientRect (). The top / / elements of the first module is going to full display in the view within the if (firstDomTop > = TopRange) {this. State. IsNavFixed && this.setState({ isNavFixed: false }) } })Copy the code

Use requestAnimationFrame to determine in real time that the anchor navigation bar is about to disappear from view

Const step = () => {let {TopRange} = this.state const navDom = Document. QuerySelector (` # NAV_ANCHOR `) / / the current element from the top of the view distance let navDomTop = navDom. GetBoundingClientRect (). The top / / If (navDomTop <= TopRange) {! this.state.isNavFixed && this.setState({ isNavFixed: Const firstDom = document.querySelector(' #content0 ') // Let firstDomTop = FirstDom. GetBoundingClientRect (). The top / / elements of the first module is going to full display in the view within the if (firstDomTop > = TopRange) {this. State. IsNavFixed && this.setState({ isNavFixed: False})} this. RequestAnimationFrameInstance = window. RequestAnimationFrame (step) / / store instance} step ()Copy the code

3. Use IntersectionObserver to asynchronously observe that the navigation bar is about to disappear in the visible area and then suck the top

let options = { root: document.querySelector('.layout-main'), threshold: [0, 1]} / / create a viewer enclosing navObserver = new Windows. IntersectionObserver (function (entries) { entries.reverse().forEach(function (entry) { if (entry.boundingClientRect.top < 0) { entry.target && entry.target.isNavShow() } else { entry.target && entry.target.isNavHidden() } }) }, options) let element = document.querySelector(`#NAV_ANCHOR`) if (element) { element.isNavShow = () => { this.setState({ isNavFixed: true }) } element.isNavHidden = () => { this.setState({ isNavFixed: False})} // Start observing this.navobserver.observe (element)}Copy the code

4, Use position: sticky layout. When the element is inside the screen, it’s described as relative, and when it rolls off the screen, it’s described as fixed.

.navigate-wrapper { position: sticky; top: 0; width: 100%; Border - top: 0.5 px solid # eeeeee; Border - bottom: 0.5 px solid # eeeeee; background: #fff; }Copy the code

Add: pro test mi 11 mobile phone MIUI 12.0.22.0 has no problem, iphoneXS Max mobile phone ios 14.3 has a problem, due to the use of the product to the user, sure enough to abandon ┭┮﹏┭┮

conclusion

The focus of the anchor function point is to monitor the scrolling state of page elements to change the navigation selection. After investigation and landing examples, it is found that there are three ways to monitor the scroll bar, real-time monitor and observer to observe elements. All three ways can achieve this function, and there are no problems in the compatibility of mobile phones. You can choose a way to implement it according to the actual product function.