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

From version 1.1
edited by wiki admin
on 2026/08/24 21:04
Change comment: There is no comment for this version
To version 10.1
edited by wiki admin
on 2026/08/26 01:04
Change comment: There is no comment for this version

Summary

Details

Page properties
Hidden
... ... @@ -1,1 +1,1 @@
1 -false
1 +true
Content
... ... @@ -1,1 +1,1 @@
1 -Article accordion for the customer-facing design. Edit the object, not this text.
1 +Article accordion. Edit the object, not this text.
XWiki.JavaScriptExtension[0]
Parse content
... ... @@ -1,0 +1,1 @@
1 +No
Code
... ... @@ -1,0 +1,260 @@
1 +/* ===========================================================================
2 + Alder Support Center - article accordion
3 + Goes in an XWiki.JavaScriptExtension object on AlderTheme.Scripts (alder wiki).
4 +
5 + Turns each top-level H2 inside #xwikicontent into a collapsible section, so a
6 + long omnibus article becomes a short list of section titles. No article needs
7 + editing: the sections come from headings the content already has.
8 +
9 + Deliberate behaviours, all reversible by disabling this object:
10 + * Sections open independently. Opening one does not close another.
11 + * The first section starts open, so the page never looks empty.
12 + * Fewer than two H2s: nothing happens, the article renders normally.
13 + * Edit/preview modes are skipped, or authors would be editing generated
14 + markup.
15 + * A link to a heading inside a collapsed section opens that section and
16 + scrolls to it, including on first load and on later hash changes.
17 + =========================================================================== */
18 +(function () {
19 + 'use strict';
20 +
21 + var MIN_SECTIONS = 2;
22 +
23 + function ready(fn) {
24 + if (document.readyState === 'loading') {
25 + document.addEventListener('DOMContentLoaded', fn);
26 + } else {
27 + fn();
28 + }
29 + }
30 +
31 + function chevron() {
32 + var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
33 + svg.setAttribute('class', 'alder-chev');
34 + svg.setAttribute('viewBox', '0 0 24 24');
35 + var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
36 + path.setAttribute('d', 'M6 9l6 6 6-6');
37 + svg.appendChild(path);
38 + return svg;
39 + }
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 + function isWikiHome() {
59 + // The employees home page has several H2s (departments, article status,
60 + // publishing workflow). Collapsing a landing page into rows hides the
61 + // thing people came for, so skip it. Matches both URL schemes this
62 + // deployment serves: /bin/view/Main/ and /wiki/<name>/view/Main/.
63 + var path = (window.location.pathname || '').replace(/\/+$/, '');
64 + return /\/view\/Main(\/WebHome)?$/.test(path);
65 + }
66 +
67 + function build() {
68 + var body = document.body;
69 + if (!body || body.className.indexOf('viewbody') === -1) {
70 + return; // edit, preview, or anything that is not a rendered page
71 + }
72 + if (isWikiHome()) { return; }
73 + var content = document.getElementById('xwikicontent');
74 + if (!content) { return; }
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 = [];
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 });
106 + }
107 + }
108 +
109 + if (eligible.length >= MIN_SECTIONS) {
110 + eligible.forEach(function (item, index) {
111 + var panel = item.panel;
112 + var heading = item.heading;
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'); }
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);
126 +
127 + // The heading is now the summary, so it must not also appear inside.
128 + panel.removeChild(heading);
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);
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; }
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 + }
177 + }
178 + if (!chosen) { return; }
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 + if (!made.length) { return; }
218 +
219 + var tools = document.createElement('div');
220 + tools.className = 'alder-tools';
221 + tools.appendChild(button('Expand all', true, made));
222 + tools.appendChild(button('Collapse all', false, made));
223 + made[0].parentNode.insertBefore(tools, made[0]);
224 +
225 + openFromHash();
226 + window.addEventListener('hashchange', openFromHash);
227 + }
228 +
229 + function button(label, open, sections) {
230 + var b = document.createElement('button');
231 + b.type = 'button';
232 + b.textContent = label;
233 + b.addEventListener('click', function () {
234 + sections.forEach(function (d) { d.open = open; });
235 + });
236 + return b;
237 + }
238 +
239 + function openFromHash() {
240 + var hash = window.location.hash;
241 + if (!hash || hash.length < 2) { return; }
242 + var id = hash.slice(1);
243 + var target = null;
244 + try {
245 + target = document.getElementById(id);
246 + } catch (e) {
247 + return;
248 + }
249 + if (!target) { return; }
250 + // Open the target itself if it is a section, plus any collapsed ancestor.
251 + var node = target;
252 + while (node && node !== document.body) {
253 + if (node.tagName === 'DETAILS') { node.open = true; }
254 + node = node.parentNode;
255 + }
256 + if (target.scrollIntoView) { target.scrollIntoView(); }
257 + }
258 +
259 + ready(build);
260 +})();
Name
... ... @@ -1,0 +1,1 @@
1 +Article accordion
Use this extension
... ... @@ -1,0 +1,1 @@
1 +always