javascriptobservablehq

observable Scrubber notebook convertion into vanilla JS


I am trying to convert observablehq 'Scrubber' notebook into vanilla JS. My current code looks like this:

<!DOCTYPE html>
<meta charset="utf-8">
<title>Scrubber</title>
<link rel="stylesheet" type="text/css" href="./inspector.css">
<body>
    <div class="test"></div>
    <script>
dates = ["2018-12-31T23:00:00.000Z","2019-01-01T23:00:00.000Z"]
function Scrubber(container, values, 
  format = value => value,
  initial = 0,
  delay = null,
  autoplay = true,
  loop = true,
  loopDelay = null,
  alternate = false) {
  values = Array.from(values);
  const form = '<form style="font: 12px var(--sans-serif); font-variant-numeric: tabular-nums; display: flex; height: 33px; align-items: center;">\
  <button name=b type=button style="margin-right: 0.4em; width: 5em;"></button>\
  <label style="display: flex; align-items: center;">\
    <input name=i type=range min=0 max=${values.length - 1} value=${initial} step=1 style="width: 180px;">\
    <output name=o style="margin-left: 0.4em;"></output>\
  </label>\
</form>';
  let frame = null;
  let timer = null;
  let interval = null;
  let direction = 1;
  function start() {
    form.b.textContent = "Pause";
    if (delay === null) frame = requestAnimationFrame(tick);
    else interval = setInterval(tick, delay);
  }
  function stop() {
    form.b.textContent = "Play";
    if (frame !== null) cancelAnimationFrame(frame), frame = null;
    if (timer !== null) clearTimeout(timer), timer = null;
    if (interval !== null) clearInterval(interval), interval = null;
  }
  function running() {
    return frame !== null || timer !== null || interval !== null;
  }
  function tick() {
    if (form.i.valueAsNumber === (direction > 0 ? values.length - 1 : direction < 0 ? 0 : NaN)) {
      if (!loop) return stop();
      if (alternate) direction = -direction;
      if (loopDelay !== null) {
        if (frame !== null) cancelAnimationFrame(frame), frame = null;
        if (interval !== null) clearInterval(interval), interval = null;
        timer = setTimeout(() => (step(), start()), loopDelay);
        return;
      }
    }
    if (delay === null) frame = requestAnimationFrame(tick);
    step();
  }
  function step() {
    form.i.valueAsNumber = (form.i.valueAsNumber + direction + values.length) % values.length;
    form.i.dispatchEvent(new CustomEvent("input", {bubbles: true}));
  }
  

  form.i.oninput = event => {
    if (event && event.isTrusted && running()) stop();
    form.value = values[form.i.valueAsNumber];
    form.o.value = format(form.value, form.i.valueAsNumber, values);
  };
  form.b.onclick = () => {
    if (running()) return stop();
    direction = alternate && form.i.valueAsNumber === values.length - 1 ? -1 : 1;
    form.i.valueAsNumber = (form.i.valueAsNumber + direction) % values.length;
    form.i.dispatchEvent(new CustomEvent("input", {bubbles: true}));
    start();
  };
  form.i.oninput();
  if (autoplay) start();
  else stop();
  Inputs.disposal(form).then(stop);
  return form;
}  
Scrubber(".test", dates)
    </script>
</body>

But I get the following error : Uncaught TypeError: Cannot set properties of undefined (setting 'oninput') at Scrubber.

It seems that this should be a new function but how can I do that?

Thank you in advance for your help.


Solution

  • form was just a string, which was getting undefined. i appended the form string in DOM and accessed form node, maybe its gonna help you.

    <!DOCTYPE html>
    <meta charset="utf-8">
    <title>Scrubber</title>
    <link rel="stylesheet" type="text/css" href="./inspector.css">
    <body>
        <div class="test"></div>
        <script>
    dates = ["2018-12-31T23:00:00.000Z","2019-01-01T23:00:00.000Z"]
    function Scrubber(container, values, 
      format = value => value,
      initial = 0,
      delay = null,
      autoplay = true,
      loop = true,
      loopDelay = null,
      alternate = false) {
      values = Array.from(values);
      const formString = '<form style="font: 12px var(--sans-serif); font-variant-numeric: tabular-nums; display: flex; height: 33px; align-items: center;">\
      <button name=b type=button style="margin-right: 0.4em; width: 5em;"></button>\
      <label style="display: flex; align-items: center;">\
        <input name=i type=range min=0 max=${values.length - 1} value=${initial} step=1 style="width: 180px;">\
        <output name=o style="margin-left: 0.4em;"></output>\
      </label>\
    </form>';
    
        document.querySelector(container).innerHTML = formString;
    
        const form = document.querySelector(container + ' form');
    
      let frame = null;
      let timer = null;
      let interval = null;
      let direction = 1;
      function start() {
        form.b.textContent = "Pause";
        if (delay === null) frame = requestAnimationFrame(tick);
        else interval = setInterval(tick, delay);
      }
      function stop() {
        form.b.textContent = "Play";
        if (frame !== null) cancelAnimationFrame(frame), frame = null;
        if (timer !== null) clearTimeout(timer), timer = null;
        if (interval !== null) clearInterval(interval), interval = null;
      }
      function running() {
        return frame !== null || timer !== null || interval !== null;
      }
      function tick() {
        if (form.i.valueAsNumber === (direction > 0 ? values.length - 1 : direction < 0 ? 0 : NaN)) {
          if (!loop) return stop();
          if (alternate) direction = -direction;
          if (loopDelay !== null) {
            if (frame !== null) cancelAnimationFrame(frame), frame = null;
            if (interval !== null) clearInterval(interval), interval = null;
            timer = setTimeout(() => (step(), start()), loopDelay);
            return;
          }
        }
        if (delay === null) frame = requestAnimationFrame(tick);
        step();
      }
      function step() {
        form.i.valueAsNumber = (form.i.valueAsNumber + direction + values.length) % values.length;
        form.i.dispatchEvent(new CustomEvent("input", {bubbles: true}));
      }
      
    
      form.i.oninput = (event) => {
        if (event && event.isTrusted && running()) stop();
        form.value = values[form.i.valueAsNumber];
        form.o.value = format(form.value, form.i.valueAsNumber, values);
      };
    
      form.b.onclick = () => {
        if (running()) return stop();
        direction = alternate && form.i.valueAsNumber === values.length - 1 ? -1 : 1;
        form.i.valueAsNumber = (form.i.valueAsNumber + direction) % values.length;
        form.i.dispatchEvent(new CustomEvent("input", {bubbles: true}));
        start();
      };
      form.i.oninput();
      if (autoplay) start();
      else stop();
      Inputs.disposal(form).then(stop);
      return form;
    }  
    Scrubber(".test", dates)
        </script>
    </body>
    

    There are other issues with the code, but original issue is fixed and slider is working now.