Last modified by wiki admin on 2026/08/26 01:04

From version 9.1
edited by wiki admin
on 2026/08/24 23:09
Change comment: There is no comment for this version
To version 7.1
edited by wiki admin
on 2026/08/24 22:31
Change comment: There is no comment for this version

Summary

Details

XWiki.JavaScriptExtension[0]
Code
... ... @@ -38,23 +38,6 @@
38 38   return svg;
39 39   }
40 40  
41 - function sectionRoot(content) {
42 - // Content is sometimes wrapped in one container (an {{html}} macro's div,
43 - // for instance). Descend through single-child wrappers so the section
44 - // blocks are found, but only a couple of levels -- deeper than that and we
45 - // are guessing.
46 - var el = content;
47 - for (var depth = 0; depth < 3; depth++) {
48 - var kids = el.children;
49 - if (kids.length === 1 && kids[0].tagName === 'DIV' && kids[0].children.length > 1) {
50 - el = kids[0];
51 - } else {
52 - break;
53 - }
54 - }
55 - return el;
56 - }
57 -
58 58   function isWikiHome() {
59 59   // The employees home page has several H2s (departments, article status,
60 60   // publishing workflow). Collapsing a landing page into rows hides the
... ... @@ -73,147 +73,70 @@
73 73   var content = document.getElementById('xwikicontent');
74 74   if (!content) { return; }
75 75  
76 - // XWiki renders each section as its own <div class="f-accordion-panel">,
77 - // with the section heading as the first child and everything else after it:
78 - //
79 - // <div class="f-accordion-panel" data-controller="accordion">
80 - // <h2>Arm/Disarm</h2> <- section heading
81 - // <h1>Arm</h1><p>...</p> <- content, including Helpjuice's inverted
82 - // </div> h1 "subsections"
83 - //
84 - // So the section container already exists and we only need to make it
85 - // collapsible. Two earlier versions of this script tried to rebuild sections
86 - // by grouping sibling headings -- first from content.children (headings are
87 - // nested, so it found none), then by grouping headings that share a parent
88 - // (each heading has its OWN panel, so every group had one item and the
89 - // script bailed silently). Both looked fine on a hand-written preview page.
90 - var made = [];
59 + // Find the element the section headings actually live in. XWiki wraps
60 + // rendered content in its own container (.f-accordion-panel on this
61 + // instance), so the headings are NOT direct children of #xwikicontent --
62 + // an earlier version of this script looked only at content.children, found
63 + // nothing on every real article, and silently did nothing. The Preview
64 + // page hid that, because hand-written wiki markup does put headings at the
65 + // top level.
66 + var found = content.querySelectorAll('h2');
67 + if (found.length < MIN_SECTIONS) { return; }
91 91  
92 - // Helpjuice exported three different widget wrappers -- f-accordion-panel
93 - // (25 articles), helpjuice-tab (71) and helpjuice-accordion (68) -- and the
94 - // migration carried all of them across as raw HTML. Rather than hardcode
95 - // class names that would miss the next variant, look for the shape they
96 - // share: a wrapper element whose FIRST element is a heading. That is a
97 - // section container regardless of what it is called.
98 - var root = sectionRoot(content);
99 - var eligible = [];
100 - var kids = root.children;
101 - for (var p = 0; p < kids.length; p++) {
102 - var first = kids[p].firstElementChild;
103 - if (first && /^H[1-4]$/.test(first.tagName) &&
104 - (first.textContent || '').trim()) {
105 - eligible.push({ panel: kids[p], heading: first });
69 + // Group headings that share a parent, using the parent holding the most of
70 + // them. Taking every h2 regardless of depth would tear headings out of
71 + // tables and macro boxes they belong to.
72 + var byParent = [];
73 + for (var f = 0; f < found.length; f++) {
74 + var parent = found[f].parentNode;
75 + var slot = null;
76 + for (var b = 0; b < byParent.length; b++) {
77 + if (byParent[b].parent === parent) { slot = byParent[b]; break; }
106 106   }
79 + if (!slot) { slot = { parent: parent, items: [] }; byParent.push(slot); }
80 + slot.items.push(found[f]);
107 107   }
82 + byParent.sort(function (a, b) { return b.items.length - a.items.length; });
108 108  
109 - if (eligible.length >= MIN_SECTIONS) {
110 - eligible.forEach(function (item, index) {
111 - var panel = item.panel;
112 - var heading = item.heading;
84 + var container = byParent[0].parent;
85 + var headings = byParent[0].items;
86 + if (headings.length < MIN_SECTIONS) { return; }
113 113  
114 - var details = document.createElement('details');
115 - details.className = 'alder-sec';
116 - if (index === 0) { details.open = true; }
117 - if (heading.id) { details.id = heading.id; heading.removeAttribute('id'); }
88 + var made = [];
89 + headings.forEach(function (h2, index) {
90 + var details = document.createElement('details');
91 + details.className = 'alder-sec';
92 + if (index === 0) { details.open = true; }
118 118  
119 - var summary = document.createElement('summary');
120 - var title = document.createElement('span');
121 - title.className = 'alder-sec-title';
122 - title.textContent = (heading.textContent || '').trim();
123 - summary.appendChild(title);
124 - summary.appendChild(chevron());
125 - details.appendChild(summary);
94 + var summary = document.createElement('summary');
95 + var title = document.createElement('span');
96 + title.className = 'alder-sec-title';
97 + title.textContent = (h2.textContent || '').trim();
98 + summary.appendChild(title);
99 + summary.appendChild(chevron());
100 + details.appendChild(summary);
126 126  
127 - // The heading is now the summary, so it must not also appear inside.
128 - panel.removeChild(heading);
102 + var wrap = document.createElement('div');
103 + wrap.className = 'alder-sec-body';
104 + details.appendChild(wrap);
129 129  
130 - var wrap = document.createElement('div');
131 - wrap.className = 'alder-sec-body';
132 - while (panel.firstChild) { wrap.appendChild(panel.firstChild); }
133 - details.appendChild(wrap);
106 + // Keep the heading's id on the details element so existing anchors and
107 + // the table-of-contents macro still resolve to something.
108 + if (h2.id) { details.id = h2.id; h2.removeAttribute('id'); }
134 134  
135 - // Keep the panel as the outer element: it carries XWiki's own
136 - // data-controller, and removing it could break behaviour we did not
137 - // write and cannot see.
138 - panel.appendChild(details);
139 - made.push(details);
140 - });
141 - } else {
142 - // No Helpjuice panels: build sections from headings. Do NOT assume h2.
143 - // Across the migrated corpus the section level varies -- 304 articles use
144 - // h2, 67 use h3, 13 use h1 -- because Helpjuice authors picked levels by
145 - // appearance rather than structure. Assuming h2 left 99 articles with no
146 - // rows at all.
147 - //
148 - // Rule: use the SHALLOWEST level that appears at least twice among
149 - // headings sharing a parent. That is the level acting as "section" in
150 - // this particular article; anything deeper is a subsection and travels
151 - // inside.
152 - var chosen = null;
153 - for (var level = 1; level <= 4 && !chosen; level++) {
154 - // A heading with no text (Helpjuice left a few) would become a row
155 - // with a blank title, which is worse than no row at all.
156 - var all = content.querySelectorAll('h' + level);
157 - var atLevel = [];
158 - for (var t = 0; t < all.length; t++) {
159 - if ((all[t].textContent || '').trim()) { atLevel.push(all[t]); }
160 - }
161 - if (atLevel.length < MIN_SECTIONS) { continue; }
110 + container.insertBefore(details, h2);
162 162  
163 - var groups = [];
164 - for (var i = 0; i < atLevel.length; i++) {
165 - var parent = atLevel[i].parentNode;
166 - var slot = null;
167 - for (var g = 0; g < groups.length; g++) {
168 - if (groups[g].parent === parent) { slot = groups[g]; break; }
169 - }
170 - if (!slot) { slot = { parent: parent, items: [] }; groups.push(slot); }
171 - slot.items.push(atLevel[i]);
172 - }
173 - groups.sort(function (a, b) { return b.items.length - a.items.length; });
174 - if (groups[0].items.length >= MIN_SECTIONS) {
175 - chosen = { level: level, container: groups[0].parent, items: groups[0].items };
176 - }
112 + // Move the heading and everything up to the next h2 into the body.
113 + var node = h2;
114 + while (node && !(node !== h2 && node.tagName === 'H2')) {
115 + var next = node.nextSibling;
116 + wrap.appendChild(node);
117 + node = next;
118 + if (node && node.nodeType === 1 && node.tagName === 'H2') { break; }
177 177   }
178 - if (!chosen) { return; }
120 + made.push(details);
121 + });
179 179  
180 - var container = chosen.container;
181 - chosen.items.forEach(function (heading, index) {
182 - var details = document.createElement('details');
183 - details.className = 'alder-sec';
184 - if (index === 0) { details.open = true; }
185 -
186 - var summary = document.createElement('summary');
187 - var title = document.createElement('span');
188 - title.className = 'alder-sec-title';
189 - title.textContent = (heading.textContent || '').trim();
190 - summary.appendChild(title);
191 - summary.appendChild(chevron());
192 - details.appendChild(summary);
193 -
194 - var wrap = document.createElement('div');
195 - wrap.className = 'alder-sec-body';
196 - details.appendChild(wrap);
197 -
198 - if (heading.id) { details.id = heading.id; heading.removeAttribute('id'); }
199 - container.insertBefore(details, heading);
200 -
201 - // Absorb everything up to the next heading at this level or shallower.
202 - // Stopping only at the same tag would let a shallower heading (a real
203 - // structural break) get swallowed into the section above it.
204 - var node = heading;
205 - while (node) {
206 - var next = node.nextSibling;
207 - wrap.appendChild(node);
208 - node = next;
209 - if (node && node.nodeType === 1 && /^H([1-6])$/.test(node.tagName)) {
210 - if (parseInt(node.tagName.slice(1), 10) <= chosen.level) { break; }
211 - }
212 - }
213 - made.push(details);
214 - });
215 - }
216 -
217 217   if (!made.length) { return; }
218 218  
219 219   var tools = document.createElement('div');
... ... @@ -220,7 +220,7 @@
220 220   tools.className = 'alder-tools';
221 221   tools.appendChild(button('Expand all', true, made));
222 222   tools.appendChild(button('Collapse all', false, made));
223 - made[0].parentNode.insertBefore(tools, made[0]);
129 + container.insertBefore(tools, made[0]);
224 224  
225 225   openFromHash();
226 226   window.addEventListener('hashchange', openFromHash);