We write front-end which works. Or how to become an engineer from a developer. Part 2

So, in the second part we start implementation. First, let's decide on technology. I choose web components. The component approach, native api, is easy to reuse and debug.

Step 1 - Describe the End Positions


The tag for our virtual viewport will be called custom-viewport. So, first we describe the general properties for the viewport:

custom-viewport {
min-height: 50vh;
max-height: 100vh;
width: 100%;
position: absolute;
bottom: 0;
overflow: hidden;
transform-origin: 50% 100% 0;
}

Position inited:

custom-viewport[data-mode = "inited"] {
transform: translateY(calc(100% - 50vh));
transition: transform 1s;
}

Position opened:

custom-viewport[data-mode = "opened"] {
transform: translateY(0);
transition: transform 1s;
overflow-y: scroll;
}

Deleted position:

custom-viewport[data-mode = "deleted"] {
transform: translateY(100%);
transition: transform 1s;
}


Step 2 - start writing custom-viewport component


class CustomViewport extends HTMLElement {
 constructor() {
  super();
 }
}

We implement dragUp / dragDown events

class CustomViewport extends HTMLElement {
 constructor() {
  super();
 }
 connectedCallback() {
  this.addEventListener("touchstart", ev => {
   this.firstTouch = ev.touches[0];
  });
  this.addEventListener("touchmove", ev => {
   this.deltaY = ev.touches[0].clientY - this.firstTouch.clientY;
   return this.deltaY > 0 ? this.dragDown(ev) : this.dragUp(ev);
  });
 }
 dragUp(ev) {}
 dragDown(ev) {}
}

Schematically, the code above can be described as follows.



So, now we can distinguish between dragUp / dragDown events. The next utility is the calculation of the power reserve.

class CustomViewport extends HTMLElement {
 constructor() {
  super();
  this.VIEWPORT_HEIGHT = window.innerHeight; // +
 }
 connectedCallback() {
  this.addEventListener("touchstart", ev => {
   this.firstTouch = ev.touches[0];
   const rect = this.getBoundingClientRect(); // +
   const { height, top } = rect; // +
   this.bottomOffsetBeforeDragging = (height + top) - this.VIEWPORT_HEIGHT; // +
  });
  this.addEventListener("touchmove", ev => {
   this.deltaY = ev.touches[0].clientY - this.firstTouch.clientY;
   return this.deltaY > 0 ? this.dragDown() : this.dragUp();
  });
 }
 dragUp() {}
 dragDown() {}
 isBottomOffset() { // +
   return (this.bottomOffsetBeforeDragging + this.deltaY) > 0; // +
 } // +
}

Here we first remember how much power reserve we had at the moment the movement started, and then we simply add deltaY to this value and see if we can move up or not.

Actually logic dragUp:

...
dragUp() {
 if(this.isBottomOffset()) {
  //  
  return;
 }
 this.style.transform = 'translateY(0)';
}
...

We write a method that will move the viewport:

class CustomViewport extends HTMLElement {
 constructor() {
  super();
  this.VIEWPORT_HEIGHT = window.innerHeight;
 }
 connectedCallback() {
  this.addEventListener("touchstart", ev => {
   this.firstTouch = ev.touches[0];
   const rect = this.getBoundingClientRect();
   const { height, top } = rect;
   this.bottomOffsetBeforeDragging = (height + top) - this.VIEWPORT_HEIGHT;
   this.lastPosY = this.bottomOffsetBeforeDragging - this.scrollTop; // +
  });
   ...
 }
translateY() { // +
  const pixels = this.deltaY + this.lastPosY; // +
  this.style.transform = `translateY(${pixels}px)`; // +
  this.style.transition = 'none'; // +
 } // +
...
}

Let us examine in more detail what this.lastPosY is and how it is calculated. If in css we wrote transform: translateY (calc (100% - 50vh)); where 100% is the height of the virtual viewport itself, and 50vh is half the height of the real viewport, and this fits well for a static description of the position, then it is more convenient to operate with absolute values ​​to calculate the movement in the dynamics, we are doing these transformations here.

So this.lastPosY is the amount of movement of the virtual viewport in pixels at the start of the movement, it is to it that we add this.deltaY and get a new viewport position.

Since we defined the properties:

bottom: 0;
transform-origin: 50% 100% 0;

then our coordinate system for counting the movement of the viewport will take the form



We describe dragDown:

...
dragDown() {
 if(this.lastPosY < 0) {
  return;
 }
 this.translateY();
}
...

Actually dragEnd event:

class CustomViewport extends HTMLElement {
 constructor() {
  super();
  this.VIEWPORT_HEIGHT = window.innerHeight;
 }
 connectedCallback() {
  this.addEventListener("touchend", ev => { // +
   const { mode: currentMode } = this.dataset; // +
   this.style = null; // +
   if (Math.abs(deltaY) < 10) { // +
    this.dataset.mode = currentMode; // +
    return; // +
   } // +
    if (deltaY > 0) { // +
     if (currentMode === "inited") { // +
       this.dataset.mode = "deleted"; // +
       return; // +
      } // +
      this.dataset.mode = "inited"; // +
      return; // +
    } // +
    this.dataset.mode = "opened"; // +
  }); // +
   ...

In the line if (Math.abs (deltaY) <10) we indicate that if you have moved less than 10 pixels, leave the current position.

As a result, we should get a component like


class CustomViewport extends HTMLElement {
 constructor() {
  super();
  this.VIEWPORT_HEIGHT = window.innerHeight;
 }
 connectedCallback() {
  this.addEventListener("touchstart", ev => {
   this.firstTouch = ev.touches[0];
   const rect = this.getBoundingClientRect();
   const { height, top } = rect;
   this.bottomOffsetBeforeDragging = (height + top) - this.VIEWPORT_HEIGHT;
   this.lastPosY = this.bottomOffsetBeforeDragging - this.scrollTop;
  });
  this.addEventListener("touchmove", ev => {
   this.deltaY = ev.touches[0].clientY - this.firstTouch.clientY;
   return this.deltaY > 0 ? this.dragDown() : this.dragUp();
  });
  this.addEventListener("touchend", ev => {
   const { mode: currentMode } = this.dataset;
   this.style = null;
   if (Math.abs(this.deltaY) < 10) {
    this.dataset.mode = currentMode;
    return;
   }
    if (this.deltaY > 0) {
     if (currentMode === "inited") {
       this.dataset.mode = "deleted";
       return;
      }
      this.dataset.mode = "inited";
      return;
    }
    this.dataset.mode = "opened";
  });
 }
 dragUp() {
  if(this.isBottomOffset()) {
   this.translateY();
   return;
  }
  this.style.transform = 'translateY(0)';
}
 dragDown() {
   if(this.lastPosY < 0) {
   return;
  }
  this.translateY();
}
translateY() {
  const pixels = this.deltaY + this.lastPosY;
  this.style.transform = `translateY(${pixels}px)`;
  this.style.transition = 'none';
 }
 isBottomOffset() {
   return (this.bottomOffsetBeforeDragging + this.deltaY) > 0;
 }
}

customElements.define('custom-viewport', CustomViewport);

This code is not a complete implementation, but only a prototype. A more detailed study of the scroll, debounces, any other optimizations, touchcancel - left to the reader.

All Articles