1/**
2 * Web component for searchindex manager plugin
3 *
4 * @author Andreas Gohr <andi@splitbrain.org>
5 * @author Symon Bent <hendrybadao@gmail.com>
6 */
7
8class SearchIndexManager extends HTMLElement {
9    #pages = null;
10    #page = null;
11    #url = null;
12    #done = 1;
13    #count = 0;
14    #$msg = null;
15    #$buttons = null;
16    #force = '';
17    #lang = {
18        rebuild: '*Rebuild Index',
19        rebuild_tip: '*Clears the current index and then adds all pages from scratch.',
20        update: '*Update Index',
21        update_tip: '*Updates the index for any changed pages since the last update.',
22        finding: '*Finding pages...',
23        pages: '*Found %d pages.',
24        indexing: '*Indexing',
25        indexed: '*indexed',
26        notindexed: '*not indexed',
27        done: '*Finished indexing.',
28        clearing: '*Clearing index...',
29    };
30
31    connectedCallback() {
32        this.#lang = {...this.#lang, ...JSON.parse(this.getAttribute('lang'))};
33        this.#url = this.getAttribute('url');
34        this.#render();
35        this.#$msg = this.querySelector('.msg');
36        this.#$buttons = this.querySelector('.buttons');
37
38        this.querySelector('.rebuild').addEventListener('click', () => this.#rebuild());
39        this.querySelector('.update').addEventListener('click', () => this.#update());
40    }
41
42    /**
43     * Render the component HTML
44     */
45    #render() {
46        this.innerHTML = `
47            <div class="buttons">
48                <input type="button" class="button rebuild" value="${this.#lang.rebuild}">
49                <p>${this.#lang.rebuild_tip}</p>
50                <input type="button" class="button update" value="${this.#lang.update}">
51                <p>${this.#lang.update_tip}</p>
52            </div>
53            <div class="msg"></div>
54        `;
55    }
56
57    /**
58     * Gives textual feedback
59     */
60    #message(text) {
61        if (text.charAt(0) !== '<') {
62            text = `<p>${text}</p>`;
63        }
64        this.#$msg.innerHTML = text;
65    }
66
67    /**
68     * Send a POST request to the ajax endpoint
69     */
70    async #post(params) {
71        const response = await fetch(this.#url, {
72            method: 'POST',
73            headers: {
74                'Content-Type': 'application/x-www-form-urlencoded',
75            },
76            body: params
77        });
78        return response.json();
79    }
80
81    /**
82     * Starts the indexing of a page.
83     */
84    async #index() {
85        if (this.#page) {
86            const indexed = await this.#post(`call=indexpage&page=${encodeURIComponent(this.#page)}&force=${this.#force}`);
87            const wait = 250;
88            // next page from queue
89            this.#page = this.#pages.shift();
90            this.#done++;
91
92            const msg = indexed ? this.#lang.indexed : this.#lang.notindexed;
93            const status = `<p class="status">${msg}</p>`;
94            this.#message(`<p>${this.#lang.indexing} ${this.#done}/${this.#count}</p><p class="name">${this.#page}</p>${status}`);
95            // next index run
96            setTimeout(() => this.#index(), wait);
97        } else {
98            this.#finished();
99        }
100    }
101
102    /**
103     * Called when indexing is complete
104     */
105    #finished() {
106        this.#throbberOff();
107        this.#message(this.#lang.done);
108        setTimeout(() => {
109            this.#message('');
110            this.#$buttons.style.display = '';
111        }, 3000);
112    }
113
114    /**
115     * Cleans the index (ready for complete rebuild)
116     */
117    async #clear() {
118        this.#message(this.#lang.clearing);
119        const success = await this.#post('call=clearindex');
120        if (!success) {
121            this.#message(this.#lang.clearing + ' - failed, retrying...');
122            // retry
123            setTimeout(() => this.#clear(), 5000);
124        } else {
125            // start indexing
126            this.#force = 'true';
127            setTimeout(() => this.#index(), 1000);
128        }
129    }
130
131    /**
132     * Starts a full rebuild (clear + reindex)
133     */
134    #rebuild() {
135        this.#update(true);
136    }
137
138    /**
139     * Starts the index update
140     */
141    async #update(rebuild = false) {
142        this.#done = 1;
143        this.#$buttons.style.display = 'none';
144        this.#throbberOn();
145        this.#message(this.#lang.finding);
146        this.#pages = await this.#post('call=pagelist');
147        if (this.#pages.length) {
148            this.#count = this.#pages.length;
149            this.#message(this.#lang.pages.replace(/%d/, this.#pages.length));
150
151            // move the first page from the queue
152            this.#page = this.#pages.shift();
153
154            // complete index rebuild?
155            if (rebuild === true) {
156                this.#clear();
157            } else {
158                this.#force = '';
159                // just start indexing immediately
160                setTimeout(() => this.#index(), 1000);
161            }
162        } else {
163            this.#finished();
164        }
165    }
166
167    /**
168     * Add a throbber image
169     */
170    #throbberOn() {
171        this.#$msg.classList.add('updating');
172    }
173
174    /**
175     * Stop the throbber
176     */
177    #throbberOff() {
178        this.#$msg.classList.remove('updating');
179    }
180}
181
182customElements.define('searchindex-manager', SearchIndexManager);
183