Public
Edited
Jun 23, 2023
Fork of Scrubber
Importers
Insert cell
Insert cell
Insert cell
viewof i = new CustomScrubber(numbers)
Insert cell
numbers = Array.from({start: 5, length: 256}, (_, i) => i + i)
Insert cell
md`The current value of *i* is ${i}.`
Insert cell
Insert cell
Insert cell
Insert cell
new CustomScrubber(numbers, {autoplay: false})
Insert cell
Insert cell
new CustomScrubber(numbers, {loop: false})
Insert cell
Insert cell
new CustomScrubber(numbers, {loop: false, alternate: true})
Insert cell
Insert cell
new CustomScrubber(["red", "green", "blue"], {delay: 1000})
Insert cell
Insert cell
new CustomScrubber(numbers, {initial: numbers.length - 1, loopDelay: 1000})
Insert cell
Insert cell
dates = Array.from({length: 365}, (_, i) => {
const date = new Date(2019, 0, 1);
date.setDate(i + 1);
return date;
})
Insert cell
viewof date = new CustomScrubber(dates, {
autoplay: false,
format: date => date.toLocaleString("en", {month: "long", day: "numeric"})
})
Insert cell
x = new CustomScrubber(numbers, {loop: false, alternate: true})

Insert cell
viewof y = x.form
Insert cell
x.setValue(172)
Insert cell
{
y
return x.getValue()
}
Insert cell
Insert cell
Insert cell
class CustomScrubber {
constructor(values, {
format = value => value,
initial = 0,
direction = 1,
delay = null,
autoplay = true,
loop = true,
loopDelay = null,
alternate = false
} = {}) {
this.values = Array.from(values);
this.format = format;
this.direction = direction;
this.delay = delay;
this.autoplay = autoplay;
this.loop = loop;
this.loopDelay = loopDelay;
this.alternate = alternate;

this.form = document.createElement('form');
this.form.style.cssText = "font: 12px var(--sans-serif); font-variant-numeric: tabular-nums; display: flex; height: 33px; align-items: center;";

const button = document.createElement('button');
button.name = 'b';
button.type = 'button';
button.style.cssText = "margin-right: 0.4em; width: 5em;";
this.form.appendChild(button);
this.b = button;

const label = document.createElement('label');
label.style.cssText = "display: flex; align-items: center;";
this.form.appendChild(label);

const input = document.createElement('input');
input.name = 'i';
input.type = 'range';
input.min = 0;
input.max = this.values.length - 1;
input.value = initial;
input.step = 1;
input.style.cssText = "width: 180px;";
label.appendChild(input);
this.i = input;

const output = document.createElement('output');
output.name = 'o';
output.style.cssText = "margin-left: 0.4em;";
label.appendChild(output);
this.o = output;

this.frame = null;
this.timer = null;
this.interval = null;

this.i.oninput = event => {
if (event && event.isTrusted && this.running()) this.stop();
this.value = this.values[this.i.valueAsNumber];
this.o.value = this.format(this.value, this.i.valueAsNumber, this.values);
};

this.b.onclick = () => {
if (this.running()) return this.stop();
this.direction = this.alternate && this.i.valueAsNumber === this.values.length - 1 ? -1 : 1;
this.i.valueAsNumber = (this.i.valueAsNumber + this.direction) % this.values.length;
this.i.dispatchEvent(new CustomEvent("input", { bubbles: true }));
this.start();
};

this.i.oninput();

if (this.autoplay) this.start();
else this.stop();

Inputs.disposal(this.form).then(this.stop);
//return {this, this.form};
}

start() {
this.b.textContent = "Pause";
if (this.delay === null) this.frame = requestAnimationFrame(this.tick.bind(this));
else this.interval = setInterval(this.tick.bind(this), this.delay);
}

stop() {
this.b.textContent = "Play";
if (this.frame !== null) {
cancelAnimationFrame(this.frame);
this.frame = null;
}
if (this.timer !== null) {
clearTimeout(this.timer);
this.timer = null;
}
if (this.interval !== null) {
clearInterval(this.interval);
this.interval = null;
}
}

running() {
return this.frame !== null || this.timer !== null || this.interval !== null;
}

tick() {
if (this.i.valueAsNumber === (this.direction > 0 ? this.values.length - 1 : this.direction < 0 ? 0 : NaN)) {
if (!this.loop) return this.stop();
if (this.alternate) this.direction = -this.direction;
if (this.loopDelay !== null) {
if (this.frame !== null) {
cancelAnimationFrame(this.frame);
this.frame = null;
}
if (this.interval !== null) {
clearInterval(this.interval);
this.interval = null;
}
this.timer = setTimeout(() => {
this.step();
this.start();
}, this.loopDelay);
return;
}
}
if (this.delay === null) {
this.frame = requestAnimationFrame(this.tick.bind(this));
}
this.step();
}

step() {
this.i.valueAsNumber = (this.i.valueAsNumber + this.direction + this.values.length) % this.values.length;
this.i.dispatchEvent(new CustomEvent("input", { bubbles: true }));
}

setValue(val) {
this.i.valueAsNumber = this.values.indexOf(val);
this.i.dispatchEvent(new CustomEvent("input", { bubbles: true }));
}

getValue() {
return this.value;
}
}
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more