1

Precondition

I am trying to make a number list picker that user can scroll on it. The selected number was supposed to snap to the center of the container. Once a number was selected, it should be enlarged and changed the color.


Problems

  • How can I snap the first or latest items to the center of the container?
  • Is there a CSS or JavaScript way to detect a number is snapping to the center then assign selected class on it?
  • Do not rely on any plugins.

* {
  box-sizing: border-box;
  font-family: Roboto, sans-serif;
}

.container {
  display: flex;
  flex-direction: row;
  width: 10rem;
  height: 22rem;
  border-radius: 3rem;
  border: solid 0.2rem #b2b2c2;
  background-color: #000000;
  display: flex;
  align-items: center;
  justify-content: center;
}

.scrollport {
  display: flex;
  flex-direction: column;
  flex-wrap: nowrap;
  width: 9.4rem;
  height: 22rem;
  overflow: auto;
  scroll-snap-type: y mandatory;
}

.cell {
  display: block;
  scroll-snap-align: center;
  flex-grow: 1;
  flex-shrink: 0;
  flex-basis: 33.3%;
  display: flex;
  justify-content: center;
  align-items: center;
  color: #e9e9f2;
  font-size: 2.4rem;
}

.selected {
  font-size: 3rem;
  font-weight: bold;
  color: #0073e6;
}
<div class="container">
  <div class="scrollport">
    <div class="cell">09</div>
    <div class="cell selected">10</div>
    <div class="cell">11</div>
    <div class="cell">12</div>
    <div class="cell">13</div>
    <div class="cell">14</div>
    <div class="cell">15</div>
    <div class="cell">16</div>
  </div>
</div>

Expect Result

enter image description here

Penny Liu
  • 15,447
  • 5
  • 79
  • 98
  • 2
    Something is not clear to me: do you want your number to be selected only when scrolling, without having to click on it? If so, how would you be able to select the first and last item in the list? You should also change your snippet IMO, because in current state, it shows exactly what you expect, so that people can be confused what to fix – Kaddath May 29 '19 at 08:17
  • 1
    Thanks for your comments. Give me a few minutes. I'll update my post very soon. – Penny Liu May 29 '19 at 08:26
  • These are two questions. Let's stick to one. – yunzen May 29 '19 at 09:03
  • 1
    I made an edit to my answer to answer both your questions – yunzen May 29 '19 at 10:17

2 Answers2

5

Add pseudo elements to .scrollport and style them like .cell

.scrollport:before,
.scrollport:after {
  content: '';
}
.scrollport:before,
.scrollport:after,
.cell {
  /* ... */
}

Also I created a JS function that utilizes David Walsh's debounce function to simulate a scrollend event. I then check for every cell if it's center is around the scrollport's center and set the classes accordingly

"use strict";
console.clear()

{
  const selector = 'scrollport'
  const selected = 'selected'
  const scrollports = document.getElementsByClassName(selector)

  for (const scrollport of scrollports) {
    scrollport.addEventListener('scroll', debounce(check, 250) /* simulate scrollend event */ )
  }

  function check(e) {
    // uses native elementFromPoint for better performance
    const rect = e.target.getBoundingClientRect();
    const centerCell = document.elementFromPoint(rect.left + e.target.offsetWidth / 2, rect.top + e.target.offsetHeight / 2)
    for (const cell of e.target.getElementsByClassName(selected)) {
      cell.classList.remove(selected)
    }
    centerCell.classList.add(selected)
    
    // Old version for backward compatibility
    // const rect = e.target.getBoundingClientRect();
    // for (const cell of e.target.children) {
    //   const cellRect = cell.getBoundingClientRect();
    //   const bounds = [rect.height/2 - cellRect.height/2, rect.height/2 + cellRect.height/2];
    //   const centerScrollOffset = cellRect.top - rect.top + cell.offsetHeight / 2
    //   if (bounds[0] < centerScrollOffset && centerScrollOffset < bounds[1]) {
    //     cell.classList.add(selected)
    //   } else {
    //     cell.classList.remove(selected)
    //   }
    // }
  }
}
// From: https://davidwalsh.name/javascript-debounce-function
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
function debounce(func, wait, immediate) {
  var timeout;
  return function() {
    var context = this,
      args = arguments;
    var later = function() {
      timeout = null;
      if (!immediate) func.apply(context, args);
    };
    var callNow = immediate && !timeout;
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
    if (callNow) func.apply(context, args);
  };
};
* {
  box-sizing: border-box;
  font-family: Roboto, sans-serif;
}

.container {
  display: flex;
  flex-direction: row;
  width: 10rem;
  height: 22rem;
  border-radius: 3rem;
  border: solid 0.2rem #b2b2c2;
  background-color: #000000;
  display: flex;
  align-items: center;
  justify-content: center;
}

.scrollport:before,
.scrollport:after {
  content: '';
}

.scrollport {
  display: flex;
  flex-direction: column;
  flex-wrap: nowrap;
  width: 9.4rem;
  height: 22rem;
  overflow: auto;
  scroll-snap-type: y mandatory;
}

.scrollport:before,
.scrollport:after,
.cell {
  display: block;
  scroll-snap-align: center;
  flex-grow: 1;
  flex-shrink: 0;
  flex-basis: 33.3%;
  display: flex;
  justify-content: center;
  align-items: center;
  color: #e9e9f2;
  font-size: 2.4rem;
}

.selected {
  font-size: 3rem;
  font-weight: bold;
  color: #0073e6;
}
<div class="container">
  <div class="scrollport">
    <div class="cell">09</div>
    <div class="cell selected">10</div>
    <div class="cell">11</div>
    <div class="cell">12</div>
    <div class="cell">13</div>
    <div class="cell">14</div>
    <div class="cell">15</div>
    <div class="cell">16</div>
  </div>
</div>
yunzen
  • 32,854
  • 11
  • 73
  • 106
  • How can I use arrow function in **debounce** instead of `var context = this,`? – Penny Liu Jul 11 '19 at 03:31
  • 1
    @PennyLiu I don't really know, what you are trying to achieve. Where do you want to uses an arrow function. Note: Though arrow functions are useful in many places they are not suitable for all situations. – yunzen Jul 11 '19 at 06:53
  • Thanks, I got that and here's my reference. https://medium.com/@griffinmichl/implementing-debounce-in-javascript-eab51a12311e – Penny Liu Jul 12 '19 at 09:12
  • @PennyLiu It seems to be one of the cases where arrow function won't do – yunzen Jul 12 '19 at 11:02
  • Depend on **elementFromPoint** method, using `offsetWidth` and `offsetHeight` will round the value to an integer. What about using `getBoundingClientRect().width` and `getBoundingClientRect().height` for better accuracy? – Penny Liu Aug 13 '19 at 09:34
  • @PennyLiu I don't think accuracy is an issue here. Performance might as well be. – yunzen Aug 13 '19 at 10:27
  • @PennyLiu You don't need to. It is automatically done by the JS. See here for a demonstration: https://codepen.io/HerrSerker/pen/OJLMgzr – yunzen Aug 13 '19 at 13:59
  • Thanks for your reply. Actrually, I have an extension problem from here. https://stackoverflow.com/questions/57032888/center-selected-item-inside-a-number-picker-widget-in-javascript Get user assigned default number then snap to the center when page load. However, the current case can only work on a single column. How can I do that in multiple columns with `YYYY MM DD` format? – Penny Liu Aug 13 '19 at 14:16
  • @PennyLiu I edited my codepen and added `scrollport.getElementsByClassName(selected)[0].scrollIntoView({behavior: 'smooth', block: 'center'})` – yunzen Aug 13 '19 at 14:20
-1

You can try this code here:

in this answer used jquery malihu-custom-scrollbar-plugin link and update some code here

(function($){
    $(window).on("load",function(){
        $(".scrollport").mCustomScrollbar({autoHideScrollbar:true});
    });
})(jQuery);
* {
  box-sizing: border-box;
  font-family: Roboto, sans-serif;
}

.container {
  display: flex;
  flex-direction: row;
  width: 10rem;
  height: 22rem;
  border-radius: 3rem;
  border: solid 0.2rem #b2b2c2;
  background-color: #000000;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

.scrollport {
  display: flex;
  flex-direction: column;
  flex-wrap: nowrap;
  width: 9.4rem;
  height: 22rem;
  overflow: auto;
}

.cell {
  flex-basis: 120px;
  height: 120px;
  color: #e9e9f2;
  font-size: 2.4rem;
  display: flex; 
  align-items: center;
  justify-content: center;
  padding-left: 20px;
}

.selected {
  font-size: 3rem;
  font-weight: bold;
  color: #0073e6;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.2/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/malihu-custom-scrollbar-plugin/3.1.5/jquery.mCustomScrollbar.concat.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/malihu-custom-scrollbar-plugin/3.1.5/jquery.mCustomScrollbar.css" rel="stylesheet" />

<div class="container">
  <div class="scrollport">
    <div class="cell">09</div>
    <div class="cell selected">10</div>
    <div class="cell">11</div>
    <div class="cell">12</div>
    <div class="cell">13</div>
    <div class="cell">14</div>
    <div class="cell">15</div>
    <div class="cell">16</div>
  </div>
</div>
Md. Abu Sayed
  • 2,396
  • 2
  • 18
  • 26