UNPKG

@bokeh/bokehjs

Version:

Interactive, novel data visualization

198 lines 7.06 kB
import { NumericInputView, NumericInput } from "./numeric_input"; import * as p from "../../core/properties"; import { button, div, toggle_attribute } from "../../core/dom"; const { min, max } = Math; function debounce(func, wait, immediate = false) { //func must works by side effects let timeoutId; return function (...args) { const context = this; const doLater = function () { timeoutId = undefined; if (!immediate) { func.apply(context, args); } }; const shouldCallNow = immediate && timeoutId === undefined; if (timeoutId !== undefined) { clearTimeout(timeoutId); } timeoutId = setTimeout(doLater, wait); if (shouldCallNow) { func.apply(context, args); } }; } // Inspiration from https://github.com/uNmAnNeR/ispinjs export class SpinnerView extends NumericInputView { static __name__ = "SpinnerView"; wrapper_el; btn_up_el; btn_down_el; _handles; _counter; _interval; *buttons() { yield this.btn_up_el; yield this.btn_down_el; } initialize() { super.initialize(); this._handles = { interval: undefined, timeout: undefined }; this._interval = 200; } connect_signals() { super.connect_signals(); const p = this.model.properties; this.on_change(p.disabled, () => { for (const btn of this.buttons()) { toggle_attribute(btn, "disabled", this.model.disabled); } }); } _render_input() { super._render_input(); this.btn_up_el = button({ class: "bk-spin-btn bk-spin-btn-up" }); this.btn_down_el = button({ class: "bk-spin-btn bk-spin-btn-down" }); const { input_el, btn_up_el, btn_down_el } = this; this.wrapper_el = div({ class: "bk-spin-wrapper" }, input_el, btn_up_el, btn_down_el); return this.wrapper_el; } render() { super.render(); for (const btn of this.buttons()) { toggle_attribute(btn, "disabled", this.model.disabled); btn.addEventListener("mousedown", (evt) => this._btn_mouse_down(evt)); btn.addEventListener("mouseup", () => this._btn_mouse_up()); btn.addEventListener("mouseleave", () => this._btn_mouse_leave()); } this.input_el.addEventListener("keydown", (evt) => { this._input_key_down(evt); }); this.input_el.addEventListener("keyup", () => { this.model.value_throttled = this.model.value; }); this.input_el.addEventListener("wheel", (evt) => { this._input_mouse_wheel(evt); }); this.input_el.addEventListener("wheel", debounce(() => { this.model.value_throttled = this.model.value; }, this.model.wheel_wait, false)); } remove() { this._stop_incrementation(); super.remove(); } _start_incrementation(sign) { clearInterval(this._handles.interval); this._counter = 0; const { step } = this.model; const increment_with_increasing_rate = (step) => { this._counter += 1; if (this._counter % 5 == 0) { const quotient = Math.floor(this._counter / 5); if (quotient < 10) { clearInterval(this._handles.interval); this._handles.interval = setInterval(() => increment_with_increasing_rate(step), this._interval / (quotient + 1)); } else if (quotient >= 10 && quotient <= 13) { clearInterval(this._handles.interval); this._handles.interval = setInterval(() => increment_with_increasing_rate(step * 2), this._interval / 10); } } this.increment(step); }; this._handles.interval = setInterval(() => increment_with_increasing_rate(sign * step), this._interval); } _stop_incrementation() { clearTimeout(this._handles.timeout); this._handles.timeout = undefined; clearInterval(this._handles.interval); this._handles.interval = undefined; this.model.value_throttled = this.model.value; } _btn_mouse_down(event) { event.preventDefault(); const sign = event.currentTarget === this.btn_up_el ? 1 : -1; this.increment(sign * this.model.step); this.input_el.focus(); //while mouse is down we increment at a certain rate this._handles.timeout = setTimeout(() => this._start_incrementation(sign), this._interval); } _btn_mouse_up() { this._stop_incrementation(); } _btn_mouse_leave() { this._stop_incrementation(); } _input_mouse_wheel(event) { if (this.shadow_el.activeElement === this.input_el) { event.preventDefault(); const sign = event.deltaY > 0 ? -1 : 1; this.increment(sign * this.model.step); } } _input_key_down(event) { const step = (() => { const { step, page_step_multiplier } = this.model; switch (event.key) { case "ArrowUp": return step; case "ArrowDown": return -step; case "PageUp": return page_step_multiplier * step; case "PageDown": return -page_step_multiplier * step; default: return null; } })(); if (step != null) { event.preventDefault(); this.increment(step); } } increment(step) { const { low, high } = this.model; if (this.model.value == null) { if (step > 0) { this.model.value = low != null ? low : (high != null ? min(0, high) : 0); } else if (step < 0) { this.model.value = high != null ? high : (low != null ? max(low, 0) : 0); } } else { this.model.value = this.bound_value(this.model.value + step); } } change_input() { super.change_input(); this.model.value_throttled = this.model.value; } bound_value(value) { const { low, high } = this.model; if (low != null && value < low) { return this.model.value ?? 0; } if (high != null && value > high) { return this.model.value ?? 0; } return value; } } export class Spinner extends NumericInput { static __name__ = "Spinner"; constructor(attrs) { super(attrs); } static { this.prototype.default_view = SpinnerView; this.define(({ Float, Nullable }) => ({ value_throttled: [Nullable(Float), p.unset, { readonly: true }], step: [Float, 1], page_step_multiplier: [Float, 10], wheel_wait: [Float, 100], })); this.override({ mode: "float", }); } } //# sourceMappingURL=spinner.js.map