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 = " ".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