aboutsummaryrefslogtreecommitdiff
path: root/static/ts/components/input-list.ts
blob: 31dd5158454bc3f4ce4927fb04e8f9b3a6142cf5 (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
/**
 * `<input-list />`
 *
 * A list of identical input fields, which forms a group. For example
 * useful to handle keywords.
 *
 * @category Web Components
 * @mergeTarget components
 * @module
 */
export { InputList }

/*
  TODO allow each item to be a larger unit, possibly containing multiple input
  fields.
*/
class InputList extends HTMLElement {

    el: HTMLInputElement;

    #listeners: [string, (e: Event) => void][] = [];

    constructor() {
        super();
        this.el = this.children[0].cloneNode(true) as HTMLInputElement;
    }

    connectedCallback() {
        for (let child of this.children) {
            child.remove();
        }
        this.addInstance();
    }

    createInstance(): HTMLInputElement {
        let new_el = this.el.cloneNode(true) as HTMLInputElement
        let that = this;
        new_el.addEventListener('input', function() {
            /* TODO .value is empty both if it's actually empty, but also
               for invalid input. Check new_el.validity, and new_el.validationMessage
            */
            if (new_el.value === '') {
                let sibling = (this.previousElementSibling || this.nextElementSibling)
                /* Only remove ourselves if we have siblings
                   Otherwise we just linger */
                if (sibling) {
                    this.remove();
                    (sibling as HTMLInputElement).focus();
                }
            } else {
                if (!this.nextElementSibling) {
                    that.addInstance();
                    // window.setTimeout(() => this.focus())
                    this.focus();
                }
            }
        });

        for (let [type, proc] of this.#listeners) {
            new_el.addEventListener(type, proc);
        }

        return new_el;
    }

    addInstance() {
        let new_el = this.createInstance();
        this.appendChild(new_el);
    }

    /**
     * The value from each element, except the last which should always be empty.
     * Has an unspecified type, since children:s value field might give non-strings.
     */
    get value(): any[] {
        let value_list = []
        for (let child of this.children) {
            value_list.push((child as any).value);
        }
        if (value_list[value_list.length - 1] === '') {
            value_list.pop();
        }
        return value_list
    }

    set value(new_value: any[]) {

        let all_equal = true;
        for (let i = 0; i < this.children.length; i++) {
            let sv = (this.children[i] as any).value
            all_equal
                &&= (sv == new_value[i])
                || (sv === '' && new_value[i] == undefined)
        }
        if (all_equal) return;

        /* Copy our current input elements into a dictionary.
           This allows us to only create new elements where needed
        */
        let values = new Map;
        for (let child of this.children) {
            values.set((child as HTMLInputElement).value, child);
        }

        let output_list: HTMLInputElement[] = []
        for (let value of new_value) {
            let element;
            /* Only create element if needed */
            if ((element = values.get(value))) {
                output_list.push(element)
                /* clear dictionary */
                values.set(value, false);
            } else {
                let new_el = this.createInstance();
                new_el.value = value;
                output_list.push(new_el);
            }
        }
        /* final, trailing, element */
        output_list.push(this.createInstance());

        this.replaceChildren(...output_list);
    }

    addEventListener(type: string, proc: ((e: Event) => void)) {
        // if (type != 'input') throw "Only input supported";

        this.#listeners.push([type, proc])

        for (let child of this.children) {
            child.addEventListener(type, proc);
        }
    }
}