Kami menulis front-end yang berfungsi. Atau cara menjadi insinyur dari pengembang. Bagian 2

Jadi, pada bagian kedua kita mulai implementasi. Pertama, mari kita putuskan teknologi. Saya memilih komponen web. Pendekatan komponen, api asli, mudah digunakan kembali dan debug.

Langkah 1 - Jelaskan Posisi Akhir


Tag untuk viewport virtual kami akan disebut custom-viewport. Jadi, pertama-tama kami menggambarkan properti umum untuk viewport:

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

Posisi yang diinisiasi:

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

Posisi dibuka:

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

Posisi yang dihapus:

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


Langkah 2 - mulai menulis komponen custom-viewport


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

Kami menerapkan acara dragUp / dragDown

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) {}
}

Secara skematis, kode di atas dapat dijelaskan sebagai berikut:



Jadi, sekarang kita dapat membedakan antara peristiwa dragUp / dragDown. Utilitas berikutnya adalah perhitungan cadangan daya.

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; // +
 } // +
}

Di sini kita pertama-tama mengingat berapa banyak cadangan daya yang kita miliki saat pergerakan dimulai, dan kemudian tambahkan saja nilai ini dan lihat apakah kita dapat naik atau tidak.

Sebenarnya dragUp logika:

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

Kami menulis metode yang akan memindahkan 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'; // +
 } // +
...
}

Mari kita teliti lebih detail apa ini. LastPosY itu dan bagaimana menghitungnya. Jika dalam css kami menulis transform: translateY (calc (100% - 50vh)); di mana 100% adalah ketinggian viewport virtual itu sendiri, dan 50vh adalah setengah dari viewport nyata, dan ini cocok untuk deskripsi statis posisi, maka lebih mudah untuk menggunakan nilai absolut untuk menghitung pergerakan dalam dinamika, kami melakukan transformasi ini di sini.

Jadi this.lastPosY adalah jumlah pergerakan viewport virtual dalam piksel pada awal gerakan, kami menambahkan ini.deltaY padanya dan dapatkan posisi viewport baru.

Karena kami mendefinisikan properti:

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

maka sistem koordinat kami untuk menghitung pergerakan viewport akan mengambil bentuk yang



kami jelaskan dragDown:

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

Sebenarnya acara dragEnd:

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"; // +
  }); // +
   ...

Pada baris if (Math.abs (deltaY) <10) kami menunjukkan bahwa jika Anda telah memindahkan kurang dari 10 piksel, tinggalkan posisi saat ini.

Akibatnya, kita harus mendapatkan komponen seperti


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);

Kode ini bukan implementasi lengkap, tetapi hanya sebuah prototipe. Studi yang lebih rinci tentang scroll, debounces, optimasi lainnya, touchcancel - diserahkan kepada pembaca.

All Articles