function stepper(values, config = {}) {
const minIndex = 0;
const maxIndex = values.length - 1;
const options = Object.assign({}, defaults, config);
const showText = options.format !== null;
const form = createForm(showText, options.slider, maxIndex);
let currentIndex = null;
let playing = false;
let direction = PlaybackDirection.FORWARD;
let cancelableDelay = null;
const updateText = () => {
if (showText) {
form.output.textContent = options.format(values[currentIndex], currentIndex, values);
}
};
const updateSlider = () => {
const slider = form.slider ?? form.querySelector("progress");
slider.value = currentIndex;
};
const updateButtons = () => {
form.skipStartButton.disabled = (currentIndex === minIndex);
form.skipBackwardButton.disabled = (currentIndex === minIndex) || playing;
form.playOrPauseButton.querySelector("i").className = playing ? "bi-pause-fill" : "bi-play-fill";
form.skipForwardButton.disabled = (currentIndex === maxIndex) || playing;
form.skipEndButton.disabled = (currentIndex === maxIndex);
};
const updateFormValue = () => {
form.value = values[currentIndex];
form.dispatchEvent(new CustomEvent("input"));
};
const setCurrentIndex = (newIndex) => {
currentIndex = newIndex;
updateText();
updateSlider();
updateButtons();
updateFormValue();
};
const stop = () => {
playing = false;
cancelableDelay.cancel();
};
const pause = () => {
stop();
updateButtons();
};
const pauseAt = (newIndex) => {
stop();
setCurrentIndex(newIndex); // updates buttons, i.a.
};
const move = (newIndex, duration) => {
cancelableDelay = delay(duration);
return cancelableDelay.promise.then(() => setCurrentIndex(newIndex))
.catch(() => {}); // prevents "Uncaught (in promise)" errors (reported in console, by the Observable runtime)
};
const invert = (direction) => direction * -1;
const updateDirection = () => {
if (options.alternate) {
direction = invert(direction);
}
};
const step = async (isFirstStep) => {
let newIndex = currentIndex + direction;
let duration = options.delay ?? 0;
if (isFirstStep) {
duration = options.firstStepDelay ?? duration;
}
if ((newIndex >= minIndex) && (newIndex <= maxIndex)) {
await move(newIndex, duration);
} else {
updateDirection();
if (options.loop) {
if (options.alternate) {
newIndex = currentIndex + direction;
} else {
newIndex = (direction === PlaybackDirection.FORWARD) ? minIndex : maxIndex
}
if (!isFirstStep) {
duration = options.loopDelay ?? duration;
}
await move(newIndex, duration);
} else {
pause();
}
}
}
const play = async () => {
let isFirstStep = true;
playing = true;
updateButtons();
while (playing) {
if (options.visibility !== null) {
await options.visibility().then(() => step(isFirstStep));
} else {
await step(isFirstStep);
}
isFirstStep = false;
}
};
const playOrPause = () => {
if (playing) {
pause();
} else {
play();
}
};
const handleEvents = () => {
const slider = form.slider;
if (slider !== undefined) {
slider.oninput = (event) => {
if (event.isTrusted) {
pauseAt(+form.slider.value);
}
};
}
form.skipStartButton.onclick = () => pauseAt(minIndex);
form.skipBackwardButton.onclick = () => pauseAt(currentIndex - 1);
form.playOrPauseButton.onclick = () => playOrPause();
form.skipForwardButton.onclick = () => pauseAt(currentIndex + 1);
form.skipEndButton.onclick = () => pauseAt(maxIndex);
};
setCurrentIndex(options.initial);
handleEvents();
if (options.autoplay) {
play();
}
return form;
}