1/*  DokuWiki MoaiEditor Toc.js file
2    Version : 0.5a (May 6, 2026)
3    Author  : MoaiTools <info@moaitools.org>
4    License : GPL 3 (http://www.gnu.org/licenses/gpl.html) */
5
6/*  Shows a table of contents (at the top of the editor) which scrolls both panes to the
7    chosen section. It has a header depth selector on the side to reduce the amount of
8    headers shown in large documents.
9
10    Html structure:     #moaied__toc                        -- div
11                            #moaied__toc_title              -- span
12                            #moaied__toc_dropdowns          -- div
13                                #moaied__toc_depth          -- select
14                                #moaied__toc_wrapper        -- div
15                                    #moaied__toc_sections   -- select
16                                    #moaied__toc_dummy      -- select
17*/
18MoaiEditor.ToC = class {
19
20    constructor () {
21
22        // Variables
23        this.text = '';     // Text for dummy dropdown (for illusion of main dropdown persistence)
24        this.headers = [];  // Array of {i, type, level, text, handle}
25
26        // Objects
27        this.depth = new MoaiEditor.LocalStorage('toc_depth', 5, [2,3,4,5]);
28
29        // Create the elements
30        const container = moaiEditor.createHTML('   <div id="moaied__toc" style="display:none"></div>');
31        const title     = moaiEditor.createHTML(' <label id="moaied__toc_label">Table of contents</label>');
32        const dropdowns = moaiEditor.createHTML('   <div id="moaied__toc_dropdowns"></div>');
33        const depth     = moaiEditor.createHTML('<select id="moaied__toc_depth"></select>');
34        const wrapper   = moaiEditor.createHTML('   <div id="moaied__toc_wrapper"></div>');
35        const sections  = moaiEditor.createHTML('<select id="moaied__toc_sections"></select>');
36        const dummy     = moaiEditor.createHTML('<select id="moaied__toc_dummy"></select>');
37
38        // Add children
39        container.appendChild(title);
40        container.appendChild(dropdowns);
41        dropdowns.appendChild(wrapper);
42        dropdowns.appendChild(depth);
43        wrapper.appendChild(sections);
44        wrapper.appendChild(dummy);
45
46        // Add user input event handlers
47        sections.addEventListener("change", this.onSectionChange.bind(this));
48        depth.addEventListener("change", this.onDepthChange.bind(this));
49
50        // Add tooltips
51        sections.title = "Scroll to a specific section";
52        depth.title = "Choose the depth of the table of contents";
53
54        // Populate the depth chooser (h2, h3, h4, h5)
55        for (let i of [2,3,4,5])
56            depth.appendChild(moaiEditor.createHTML('<option value="'+i+'">h'+i+'</option>'));
57
58        // Keep some handles to the HTML elements
59        this.container = container;
60        this.sections = sections;
61        this.dummy = dummy;
62        this.depthSelector = depth;
63    }
64    // ────────────────────────────────────
65    // Called whenever matches are recalculated (typically on preview updates)
66    update () {
67        let i = 0;
68        this.headers = [];
69        // Gather all matched headers
70        for (let match of moaiEditor.matches.matches)
71            if (['H1','H2','H3','H4','H5'].includes(match.type)) {
72                this.headers.push({
73                    i      : i,
74                    type   : match.type,
75                    level  : Number(match.type.substr(1,1)),
76                    text   : match.handle.textContent,
77                    handle : match.handle
78                });
79                i += 1;
80            }
81        // Update UI
82        this.redraw();
83    }
84    // ────────────────────────────────────
85    redraw () {
86
87        // Update the depth dropdown selected option to what is saved
88        const depth = this.depth.value;
89        this.depthSelector.selectedIndex = depth-2;
90
91        // Remove all previous options
92        this.sections.options.length = 0;
93
94        // If there is only one <h1> dont show it (makes the element wider because everything else will be indented)
95        var ignoreH1 = 0;
96        var count = 0;
97        for (let header of this.headers)
98            if (header.type == 'H1')
99                count += 1;
100        if (count < 2)
101            ignoreH1 = 1;
102
103        // Add an empty option first
104        this.sections.appendChild(moaiEditor.createHTML('<option></option>'));
105
106        // Add options
107        for (let header of this.headers) {
108            if (ignoreH1 == 1  &&  header.type == "H1")
109                continue;
110            if (header.level > depth)
111                continue;
112            const indent = "&nbsp;".repeat(4*(header.level-1-ignoreH1));
113            const html = moaiEditor.createHTML('<option value="' + header.i + '">' + indent + header.text + '</option>');
114            this.sections.appendChild(html);
115        }
116        // Hide the elements if there are no sections to show
117        if (this.sections.options.length < 2) {
118            this.container.style.display = 'none';
119            return;
120        }
121        // Show them otherwise
122        this.container.style.display = 'flex';
123
124        // Set dummy width
125        const width = this.sections.getBoundingClientRect().width;
126        this.dummy.style.width = width+'px';
127
128        // Set dummy value
129        this.dummy.options.length = 0;
130        this.dummy.appendChild(moaiEditor.createHTML('<option>'+this.text+'</option>'));
131    }
132    // ────────────────────────────────────
133    onDepthChange () {
134
135
136        // Store the value
137        const value = this.depthSelector.value;
138        this.depth.value = parseInt(value);
139
140        // Update the UI
141        this.text = '';
142        this.redraw();
143    }
144    // ────────────────────────────────────
145    onSectionChange () {
146
147        // Update the text in the dummy
148        const value = this.sections.value;
149        this.text = '';
150        if (value != '')
151            this.text = this.headers[value].text;
152
153        // Update the UI
154        this.redraw();
155
156        // Return if no section was selected
157        if (value == '')
158            return;
159
160        // Scroll
161        const element = this.headers[value].handle;
162        moaiEditor.scroll.toc (element);
163    }
164
165}; // End Class
166