aboutsummaryrefslogtreecommitdiff
path: root/static/ts/components/slider.ts
blob: 8be66a73e1ebb7103dc5215171acf3b808c78d12 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
/**
   <slider-input />

   A Web Component implementing a slider with a corresponding number input.

   TODO rename this file

   ### Parameters

   All of these are optional, see {@linkcode dflt} for defaults.

   #### min
   Minimum allowed value.

   #### max
   Maximum allowed value.

   #### step
   How large each step of the slider/number box should be.

   @module
*/

export { SliderInput, Attribute, dflt }

import { makeElement } from '../lib'

/** Defalut values for all attributes, if not given */
const dflt = {
    min: 0,
    max: 100,
    step: 1,
}

/** Valid attributes for SliderInput */
type Attribute = 'min' | 'max' | 'step'

/**
   Component displaying an input slider, together with a corresponding numerical
   input
*/
class SliderInput extends HTMLElement {

    /* value a string since javascript kind of expects that */
    #value = "" + dflt.min
    /** Minimum allowed value */
    min = dflt.min
    /** Maximum allowed value */
    max = dflt.max
    /** How large each step should be */
    step = dflt.step

    /** The HTML slider component */
    readonly slider: HTMLInputElement;
    /** The HTML number input component */
    readonly textIn: HTMLInputElement;

    constructor(min?: number, max?: number, step?: number, value?: number) {
        super();

        this.min = min || parseFloat(this.getAttribute('min') || "" + dflt['min']);
        this.max = max || parseFloat(this.getAttribute('max') || "" + dflt['max']);
        this.step = step || parseFloat(this.getAttribute('step') || "" + dflt['step']);
        // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/range#value
        const defaultValue
            = (this.max < this.min)
                ? this.min
                : this.min + (this.max - this.min) / 2;

        this.slider = makeElement('input', {
            type: 'range',
            min: this.min,
            max: this.max,
            step: this.step,
            value: this.value,
        }) as HTMLInputElement
        this.textIn = makeElement('input', {
            type: 'number',
            min: this.min,
            max: this.max,
            step: this.step,
            value: this.value,
        }) as HTMLInputElement

        this.slider.addEventListener('input', e => this.#propagate(e));
        this.textIn.addEventListener('input', e => this.#propagate(e));

        /* MUST be after sub components are bound */
        this.value = "" + (value || this.getAttribute('value') || defaultValue);
    }

    connectedCallback() {
        this.replaceChildren(this.slider, this.textIn);
    }

    /** ['min', 'max', 'step'] */
    static get observedAttributes(): Attribute[] {
        return ['min', 'max', 'step']
    }

    attributeChangedCallback(name: Attribute, _?: string, to?: string): void {
        if (to) {
            this.slider.setAttribute(name, to);
            this.textIn.setAttribute(name, to);
        } else {
            this.slider.removeAttribute(name);
            this.textIn.removeAttribute(name);
        }
        this[name] = parseFloat(to || "" + dflt[name])
    }

    /**
       Helper for updating the value attribute

       Event listeners are bound on both the input elements, which both simply
       call this. This procedure then updates the classes value field.

       TODO `oninput`?
    */
    #propagate(e: Event) {
        this.value = (e.target as HTMLInputElement).value;
        if (e instanceof InputEvent && this.oninput) {
            this.oninput(e);
        }
    }

    /**
       Set a new numerical value.

       A number not possible due to the current `min`, `max`, and `step`
       properties can be set and will work, the slider will however not
       properly show it, but rather the closest value it can display.
     */
    set value(value: string) {
        this.slider.value = value;
        this.textIn.value = value;
        this.#value = value;
    }

    /** Get the current numerical value */
    get value(): string {
        return this.#value;
    }

    /* TODO do we want to implement this?
     * oninput directly on the component already works
     * /
    addEventListener(type: string, proc: ((e: Event) => void)) {
    }
    */
}