MediaWiki:Gadget-DiagnosticTree.js
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/**
* DiagnosticTree.js — v6
* Copy entire contents to: MediaWiki:Gadget-DiagnosticTree.js
*
* EMBED ON ANY WIKI PAGE:
* <div class="diagnostic-tree-host"
* data-tree-page="DiagnosticTree/EarTMJ"
* data-nerve-tree-page="DiagnosticTree/EarTMJ_nerve">
* </div>
*
* data-nerve-tree-page is optional — omit it and the nerve tab
* shows as "coming soon" automatically.
*
* JSON PAGE STRUCTURE (data-tree-page):
* {
* "tree_id": "...",
* "region": "...",
* "start": "first-node-id",
* "redflags": { "emergency": [...], "urgent": [...] },
* "broad_differential": [...],
* "nodes": { ... }
* }
*/
( function () {
'use strict';
/* ══════════════════════════════════════════════════════
NODE META
══════════════════════════════════════════════════════ */
var NODE_META = {
rom: { color:'#1d4ed8', icon:'🔄', label:'Movement Screen' },
nerve_entrapment: { color:'#6d28d9', icon:'⚡', label:'Nerve Screen' },
symptom: { color:'#0f766e', icon:'💬', label:'Patient Symptom' },
examination: { color:'#15803d', icon:'🩺', label:'Examination' },
result: { color:'#15803d', icon:'✅', label:'Diagnosis' },
overlap: { color:'#78716c', icon:'🔍', label:'Inconclusive' },
neuro_referral: { color:'#b91c1c', icon:'🧠', label:'Neurological Referral' }
};
/* ══════════════════════════════════════════════════════
RED FLAG PANEL
══════════════════════════════════════════════════════ */
function RedFlagPanel( el, data, onCleared ) {
this.el = el; this.data = data; this.onCleared = onCleared;
this.state = {}; this.expanded = null;
data.emergency.forEach( function ( f ) { this.state[ f.id ] = 'pending'; }, this );
data.urgent.forEach( function ( f ) { this.state[ f.id ] = 'pending'; }, this );
this.render();
}
RedFlagPanel.prototype.allCleared = function () {
return Object.keys( this.state ).every( function ( k ) {
return this.state[ k ] === 'cleared';
}, this );
};
RedFlagPanel.prototype.render = function () {
var s = this;
var eC = this.data.emergency.every( function ( f ) { return s.state[ f.id ] === 'cleared'; } );
var uC = this.data.urgent.every( function ( f ) { return s.state[ f.id ] === 'cleared'; } );
this.el.innerHTML =
'<div class="dt-redflag-panel">' +
this._col( 'emergency', this.data.emergency, eC ) +
this._col( 'urgent', this.data.urgent, uC ) +
'</div>';
this.el.querySelectorAll( '.dt-flag-item' ).forEach( function ( el ) {
var id = el.getAttribute( 'data-id' );
el.addEventListener( 'click', function ( e ) {
if ( e.target.classList.contains( 'dt-flag-btn' ) ) return;
if ( s.state[ id ] === 'cleared' ) return;
s.expanded = s.expanded === id ? null : id;
s.render();
} );
} );
this.el.querySelectorAll( '.dt-flag-btn' ).forEach( function ( btn ) {
btn.addEventListener( 'click', function ( e ) {
e.stopPropagation();
var id = btn.getAttribute( 'data-id' );
var act = btn.getAttribute( 'data-action' );
s.state[ id ] = act === 'present' ? 'present' : 'cleared';
s.expanded = act === 'present' ? id : null;
s.render();
if ( s.allCleared() ) s.onCleared();
} );
} );
};
RedFlagPanel.prototype._col = function ( type, flags, allCleared ) {
var s = this, isE = type === 'emergency';
var cc = 'dt-flag-column' + ( isE ? '' : ' urgent' ) + ( allCleared ? ' all-cleared' : '' );
var items = flags.map( function ( f ) { return s._item( f ); } ).join( '' );
var refs = '';
flags.forEach( function ( f ) {
if ( s.state[ f.id ] === 'present' ) {
refs += '<div class="dt-flag-referral ' + ( isE ? '' : 'urgent-ref' ) + ' visible">' +
'<strong>⛔ ' + ( isE ? 'EMERGENCY ACTION' : 'URGENT REFERRAL' ) + '</strong>' +
f.action + '</div>';
}
} );
return '<div class="' + cc + '">' +
'<div class="dt-flag-col-header"><span class="header-icon">' + ( isE ? '🚨' : '⚠️' ) + '</span>' +
( isE ? 'Emergency' : 'Urgent Referral' ) + '</div>' +
'<div class="dt-flag-items">' + items + '</div>' + refs +
'<div class="dt-panel-cleared-msg">✓ All ' + ( isE ? 'emergency' : 'urgent' ) + ' flags cleared</div>' +
'</div>';
};
RedFlagPanel.prototype._item = function ( f ) {
var isCl = this.state[ f.id ] === 'cleared';
var isEx = this.expanded === f.id;
var cls = 'dt-flag-item' + ( isCl ? ' cleared' : '' ) + ( isEx ? ' expanded' : '' );
var acts = isCl ? '' :
'<div class="dt-flag-actions">' +
'<button class="dt-flag-btn present" data-id="' + f.id + '" data-action="present">Present</button>' +
'<button class="dt-flag-btn absent" data-id="' + f.id + '" data-action="absent">Absent — cleared</button>' +
'</div>';
return '<div class="' + cls + '" data-id="' + f.id + '">' +
'<div class="dt-flag-checkbox"></div>' +
'<div class="dt-flag-item-body">' +
'<div class="dt-flag-label">' + f.label + '</div>' +
'<div class="dt-flag-question">' + f.question +
'<div class="dt-flag-rationale">' + f.rationale + '</div>' +
'</div>' + acts +
'</div></div>';
};
/* ══════════════════════════════════════════════════════
BROAD DIFFERENTIAL PANEL
══════════════════════════════════════════════════════ */
function BroadDiffPanel( el, data ) {
this.el = el; this.data = data;
this.open = null; this.collapsed = false; this.locked = true;
this.render();
}
BroadDiffPanel.prototype.unlock = function () { this.locked = false; this.render(); };
BroadDiffPanel.prototype.render = function () {
var s = this;
var lockedSection = this.locked
? '<div class="dt-broad-locked-msg">⟳ Complete the emergency & urgent checklist above to unlock</div>'
: '<div class="dt-broad-grid' + ( this.collapsed ? ' hidden' : '' ) + '" id="bd-grid">' +
this.data.map( function ( d ) { return s._card( d ); } ).join( '' ) +
'</div>';
this.el.innerHTML =
'<div class="dt-broad-panel">' +
'<div class="dt-broad-header">' +
'<div class="dt-broad-header-top">' +
'<div class="dt-broad-title"><span>🔭</span> Broad Differential — Also Consider</div>' +
( !this.locked
? '<button class="dt-broad-toggle" id="bd-toggle">' + ( this.collapsed ? 'Show ▾' : 'Hide ▴' ) + '</button>'
: '' ) +
'</div>' +
'<div class="dt-epigraph">' +
'"If he does not expect the unexpected, he will not discover it — for it is difficult to discover and intractable." ' +
'<cite>— Heraclitus, Fr. 18</cite>' +
'</div>' +
'</div>' +
lockedSection +
'</div>';
var tog = this.el.querySelector( '#bd-toggle' );
if ( tog ) tog.addEventListener( 'click', function () { s.collapsed = !s.collapsed; s.render(); } );
this.el.querySelectorAll( '.dt-diff-item' ).forEach( function ( el ) {
var id = el.getAttribute( 'data-id' );
el.addEventListener( 'click', function ( e ) {
if ( e.target.classList.contains( 'dt-wiki-link' ) ) return;
s.open = s.open === id ? null : id;
s.render();
} );
} );
};
BroadDiffPanel.prototype._card = function ( d ) {
var isOpen = this.open === d.id;
var detail = isOpen
? '<div class="dt-diff-detail">' +
'<div class="dt-diff-distinguisher"><strong>Distinguishing feature</strong>' + d.distinguishing_feature + '</div>' +
'<div class="dt-diff-action"><strong>If suspected</strong>' + d.action + '</div>' +
'</div>' : '';
return '<div class="dt-diff-item' + ( isOpen ? ' open' : '' ) + '" data-id="' + d.id + '">' +
'<div class="dt-diff-confidence ' + d.confidence + '">' + d.confidence + '</div>' +
'<div class="dt-diff-name">' + d.condition + '</div>' +
'<div class="dt-diff-mimics">' + d.mimics + '</div>' +
detail + '</div>';
};
/* ══════════════════════════════════════════════════════
TREE STATE
══════════════════════════════════════════════════════ */
function TreeState( tree ) {
this.tree = tree; this.history = []; this.current = tree.start;
}
TreeState.prototype.go = function ( id ) { this.history.push( this.current ); this.current = id; };
TreeState.prototype.back = function () { if ( this.history.length ) this.current = this.history.pop(); };
TreeState.prototype.reset = function () { this.history = []; this.current = this.tree.start; };
TreeState.prototype.node = function () { return this.tree.nodes[ this.current ]; };
/* ══════════════════════════════════════════════════════
DIAGNOSTIC TREE ENGINE
══════════════════════════════════════════════════════ */
function DiagnosticTree( el, data ) {
this.el = el; this.state = new TreeState( data ); this.render();
}
DiagnosticTree.prototype.render = function () {
var s = this, node = this.state.node(), meta = NODE_META[ node.type ] || NODE_META.symptom;
var canBack = this.state.history.length > 0;
var steps = this.state.history.length + 1;
var total = Object.keys( this.state.tree.nodes ).length;
var pct = Math.min( Math.round( ( steps / total ) * 100 ), 95 );
var cardHtml;
if ( node.type === 'result' ) cardHtml = this._result( node );
else if ( node.type === 'overlap' ) cardHtml = this._overlap( node );
else if ( node.type === 'neuro_referral') cardHtml = this._neuroRef( node );
else cardHtml = this._question( node, meta );
this.el.innerHTML =
'<div class="dt-tree-header">' +
'<span class="dt-region-label">Diagnostic algorithm</span>' +
'<div class="dt-controls">' +
( canBack ? '<button class="dt-btn dt-btn-back" id="dt-back">← Back</button>' : '' ) +
'<button class="dt-btn" id="dt-reset">↺ Restart</button>' +
'</div>' +
'</div>' +
'<div class="dt-progress-wrap"><div class="dt-progress-bar" style="width:' + pct + '%"></div></div>' +
'<div class="dt-step-label">Step ' + steps + '</div>' +
cardHtml;
var b = function ( sel, fn ) {
var el = s.el.querySelector( sel );
if ( el ) el.addEventListener( 'click', fn );
};
b( '#dt-back', function () { s.state.back(); s.render(); } );
b( '#dt-reset', function () { s.state.reset(); s.render(); } );
b( '#dt-yes', function () { var n = s.state.node(); if ( n.yes ) { s.state.go( n.yes ); s.render(); } } );
b( '#dt-no', function () { var n = s.state.node(); if ( n.no ) { s.state.go( n.no ); s.render(); } } );
this.el.querySelectorAll( '.dt-division-tab' ).forEach( function ( tab ) {
tab.addEventListener( 'click', function () {
var panel = tab.getAttribute( 'data-panel' );
s.el.querySelectorAll( '.dt-division-tab' ).forEach( function ( t ) { t.classList.remove( 'active' ); } );
s.el.querySelectorAll( '.dt-division-panel' ).forEach( function ( p ) { p.classList.remove( 'active' ); } );
tab.classList.add( 'active' );
var target = s.el.querySelector( '#' + panel );
if ( target ) target.classList.add( 'active' );
} );
} );
this.el.querySelectorAll( '.dt-wiki-link' ).forEach( function ( btn ) {
btn.addEventListener( 'click', function ( e ) {
e.stopPropagation();
var page = btn.getAttribute( 'data-page' );
if ( page ) window.location.href = mw.util.getUrl( page );
} );
} );
};
DiagnosticTree.prototype._question = function ( node, meta ) {
var mv = node.movement ? '<div class="dt-tag dt-tag-blue">' + node.movement + ' (' + ( node.direction || '' ) + ')</div>' : '';
var nv = node.nerve ? '<div class="dt-tag dt-tag-purple">Nerve: ' + node.nerve + '</div>' : '';
var et = node.exam_type ? '<div class="dt-tag dt-tag-green">' + node.exam_type.replace( /_/g, ' ' ) + '</div>' : '';
var rat = node.clinical_rationale || node.distinguishing_feature || '';
var lm = node.landmark ? '<div class="dt-landmark">📍 ' + node.landmark + '</div>' : '';
var pf = node.positive_finding? '<div class="dt-positive">✓ Positive: ' + node.positive_finding + '</div>' : '';
var nf = node.negative_finding? '<div class="dt-negative">✗ Negative: ' + node.negative_finding + '</div>' : '';
var imp = node.muscles_implicated ? '<div class="dt-muscle-hint dt-implicated">Suggests: ' + node.muscles_implicated.join( ', ' ) + '</div>' : '';
var exc = node.muscles_excluded ? '<div class="dt-muscle-hint dt-excluded">Argues against: ' + node.muscles_excluded.join( ', ' ) + '</div>' : '';
return '<div class="dt-card" style="border-color:' + meta.color + '22">' +
'<div class="dt-type-badge" style="background:' + meta.color + '">' + meta.icon + ' ' + meta.label + '</div>' +
'<div class="dt-question">' + node.question + '</div>' +
( ( mv || nv || et ) ? '<div class="dt-tags">' + mv + nv + et + '</div>' : '' ) +
lm + pf + nf +
( rat ? '<div class="dt-rationale">' + rat + '</div>' : '' ) +
imp + exc +
'<div class="dt-answers">' +
'<button class="dt-answer dt-answer-yes" id="dt-yes">Yes</button>' +
'<button class="dt-answer dt-answer-no" id="dt-no">No</button>' +
'</div>' +
'</div>';
};
DiagnosticTree.prototype._result = function ( node ) {
var conf = node.confidence || 'moderate';
var cc = { high: '#15803d', moderate: '#b45309', low: '#b91c1c' }[ conf ] || '#78716c';
var also = node.also_consider && node.also_consider.length
? '<div class="dt-also">Also consider: ' + node.also_consider.join( ', ' ) + '</div>' : '';
/* Confirmatory checklist */
var confirmatoryHtml = '';
if ( node.confirmatory && node.confirmatory.length ) {
confirmatoryHtml =
'<div class="dt-confirmatory">' +
'<div class="dt-confirmatory-header">✓ Confirm at point of diagnosis</div>' +
'<div class="dt-confirmatory-items">' +
node.confirmatory.map( function ( c ) {
return '<div class="dt-confirmatory-item"><div class="dt-confirmatory-dot"></div>' + c + '</div>';
} ).join( '' ) +
'</div>' +
'</div>';
}
/* Division tabs — SCM */
var divisionHtml = '';
if ( node.division ) {
divisionHtml =
'<div class="dt-division-tabs">' +
'<button class="dt-division-tab active" data-panel="div-sternal">Sternal Division</button>' +
'<button class="dt-division-tab" data-panel="div-clavicular">Clavicular Division</button>' +
'</div>' +
'<div class="dt-division-panel active" id="div-sternal">' +
'<div class="dt-division-content">' +
'<strong>Primary symptoms:</strong> Cheek, temple, orbit, and supraorbital pain. Sore neck sometimes misattributed to lymphadenopathy.' +
'<ul>' +
'<li>Profuse tearing — often more alarming to patient than pain</li>' +
'<li>Conjunctival redness, rhinitis</li>' +
'<li>Apparent ptosis via palpebral fissure narrowing on TrP side</li>' +
'<li>Visual disturbance with strongly contrasted vertical lines (e.g. venetian blinds)</li>' +
'<li>Head tilts to side of TrPs — pain on holding head upright</li>' +
'<li>Patient prefers to lie on sore side with pillow so face does not bear weight</li>' +
'</ul>' +
'</div>' +
'</div>' +
'<div class="dt-division-panel" id="div-clavicular">' +
'<div class="dt-division-content">' +
'<strong>Any one of three may dominate:</strong> Frontal headache, postural dizziness/imbalance, or dysmetria.' +
'<ul>' +
'<li>Dizziness worsens on changing head load, lying without pillow, or quick rotation</li>' +
'<li>Turning over in bed — patient should roll head on pillow, not lift it</li>' +
'<li>Adds to car sickness or sea sickness; may report nausea or anorexia</li>' +
'<li>Transient loss of equilibrium after vigorous or quick head/neck rotation</li>' +
'<li>Rarely: hearing impaired on same side as clavicular TrPs</li>' +
'</ul>' +
'</div>' +
'</div>';
}
/* Horner confirmation */
var hornerHtml = node.division
? '<div class="dt-confirm-panel">' +
'<div class="dt-confirm-header">🔍 Confirmatory — Horner Syndrome Exclusion</div>' +
'<div class="dt-confirm-items">' +
'<div class="dt-confirm-item"><div class="dt-confirm-dot"></div>Pupils equal and reactive — no miosis</div>' +
'<div class="dt-confirm-item"><div class="dt-confirm-dot"></div>No enophthalmos — apparent ptosis is from palpebral fissure narrowing only</div>' +
'<div class="dt-confirm-item"><div class="dt-confirm-dot"></div>Ciliospinal reflex present — pinch skin on back of neck → ipsilateral pupil dilates</div>' +
'<div class="dt-confirm-item"><div class="dt-confirm-dot"></div>Extraocular movements full — no paralysis; symptoms not conversion hysteria</div>' +
'</div>' +
'</div>' : '';
/* Satellite TrPs */
var satelliteHtml = '';
if ( node.satellite_trps && node.satellite_trps.length ) {
satelliteHtml =
'<div class="dt-satellite-section">' +
'<div class="dt-satellite-header">Satellite TrPs — may activate from or with this muscle</div>' +
'<div class="dt-satellite-grid">' +
node.satellite_trps.map( function ( t ) {
return '<span class="dt-satellite-tag">' + t + '</span>';
} ).join( '' ) +
'</div>' +
'</div>';
}
/* Landing page signposting */
var landingHtml = '';
if ( node.landing_page_topics && node.landing_page_topics.length ) {
landingHtml =
'<div class="dt-landing-panel">' +
'<div class="dt-landing-header">📋 Further assessment — muscle landing page</div>' +
'<div class="dt-landing-body">' +
'The following clinical detail goes beyond the scope of this algorithm and is covered on the muscle page:' +
'<ul class="dt-landing-list">' +
node.landing_page_topics.map( function ( t ) { return '<li>' + t + '</li>'; } ).join( '' ) +
'</ul>' +
'</div>' +
'</div>';
}
/* Related page links */
var relatedHtml = node.related_pages && node.related_pages.length
? node.related_pages.map( function ( p ) {
return '<button class="dt-wiki-link satellite" data-page="' + p.page + '">' + p.label + '</button>';
} ).join( '' ) : '';
return '<div class="dt-card dt-card-result">' +
'<div class="dt-type-badge" style="background:#15803d">✅ Likely Diagnosis</div>' +
'<div class="dt-result-name">' + node.diagnosis + '</div>' +
'<div class="dt-confidence" style="color:' + cc + '">Confidence: ' + conf + '</div>' +
divisionHtml +
( node.notes ? '<div class="dt-notes">' + node.notes + '</div>' : '' ) +
( node.treatment_hint ? '<div class="dt-treatment">Treatment: ' + node.treatment_hint + '</div>' : '' ) +
( node.chapter_ref ? '<div class="dt-chapter">📖 ' + node.chapter_ref + '</div>' : '' ) +
also + confirmatoryHtml + hornerHtml + satelliteHtml + landingHtml +
( node.wiki_page ? '<button class="dt-wiki-link" data-page="' + node.wiki_page + '">Open Wiki Page →</button>' : '' ) +
relatedHtml +
'<div class="dt-broad-reminder">' +
'<span class="dt-broad-reminder-icon">🔭</span>' +
'<div><strong>Before concluding</strong>' +
'Review the broad differential panel below — several conditions in this region closely mimic this presentation.</div>' +
'</div>' +
'</div>';
};
DiagnosticTree.prototype._neuroRef = function ( node ) {
var isE = node.urgency === 'emergency';
var color = isE ? 'var(--emergency)' : 'var(--urgent)';
var bdrCl = isE ? 'var(--emergency-border)' : 'var(--urgent-border)';
var bgCl = isE ? 'var(--emergency-bg)' : 'var(--urgent-bg)';
return '<div class="dt-card dt-card-neuro-ref" style="border-color:' + bdrCl + ';background:' + bgCl + '">' +
'<div class="dt-type-badge" style="background:' + color + '">' + ( isE ? '🚨 Emergency' : '⚠️ Urgent Referral' ) + '</div>' +
'<div class="dt-neuro-title">' + node.title + '</div>' +
'<div class="dt-neuro-body">' + node.body + '</div>' +
'<div class="dt-treatment">' + node.action + '</div>' +
'</div>';
};
DiagnosticTree.prototype._overlap = function ( node ) {
var sl = node.screen_these && node.screen_these.length
? '<ul class="dt-screen-list">' +
node.screen_these.map( function ( m ) { return '<li>' + m + '</li>'; } ).join( '' ) +
'</ul>' : '';
return '<div class="dt-card dt-card-overlap">' +
'<div class="dt-type-badge" style="background:#78716c">🔍 Inconclusive</div>' +
'<div class="dt-overlap-text">' + ( node.text || '' ) + '</div>' + sl +
( node.wiki_page ? '<button class="dt-wiki-link" data-page="' + node.wiki_page + '">Open Differential Page →</button>' : '' ) +
'<div class="dt-broad-reminder">' +
'<span class="dt-broad-reminder-icon">🔭</span>' +
'<div><strong>Before concluding</strong>' +
'Review the broad differential panel below — several conditions in this region closely mimic these findings.</div>' +
'</div>' +
'</div>';
};
/* ══════════════════════════════════════════════════════
BOOT SHELL — builds key toggle, red flags, algo, broad diff
══════════════════════════════════════════════════════ */
function bootShell( hostEl, treeData ) {
var shell = document.createElement( 'div' );
shell.className = 'dt-shell';
hostEl.appendChild( shell );
var hasNerveTree = !!hostEl.getAttribute( 'data-nerve-tree-page' );
/* 0 — Key toggle tabs */
var keyToggle = document.createElement( 'div' );
keyToggle.className = 'dt-key-toggle';
keyToggle.innerHTML =
'<button class="dt-key-tab pain-key active" id="dt-tab-pain">' +
'<span class="tab-icon">🩺</span> Pain Pattern Key' +
'</button>' +
'<button class="dt-key-tab nerve-key' + ( hasNerveTree ? '' : ' coming-soon' ) + '" id="dt-tab-nerve"' +
( hasNerveTree ? '' : ' title="Nerve entrapment key coming soon"' ) + '>' +
'<span class="tab-icon">⚡</span> Nerve Entrapment Key' +
( hasNerveTree ? '' : '<span class="tab-soon">coming soon</span>' ) +
'</button>';
shell.appendChild( keyToggle );
/* 1 — Red flag panel (shared) */
var panelEl = document.createElement( 'div' );
shell.appendChild( panelEl );
/* 2 — Pain algorithm section */
var algoSection = document.createElement( 'div' );
algoSection.className = 'dt-algo-section locked';
algoSection.innerHTML =
'<div class="dt-algo-header">' +
'<div class="dt-algo-header-left">🩺 Pain Pattern Algorithm</div>' +
'<div class="dt-algo-header-right">Complete checklist above to unlock</div>' +
'</div>' +
'<div class="dt-algo-body" id="dt-algo-body">' +
'<div class="dt-tree-locked-msg">Work through all emergency and urgent flags above before proceeding.</div>' +
'</div>';
shell.appendChild( algoSection );
/* 2b — Nerve section (placeholder or loaded) */
var nerveSection = document.createElement( 'div' );
nerveSection.style.display = 'none';
if ( !hasNerveTree ) {
nerveSection.innerHTML =
'<div class="dt-algo-section" style="border-color:var(--nerve-header-border)">' +
'<div class="dt-algo-header" style="background:var(--nerve-header)">' +
'<div class="dt-algo-header-left">⚡ Nerve Entrapment Key</div>' +
'</div>' +
'<div class="dt-algo-body">' +
'<div class="dt-tree-locked-msg" style="color:#6d28d9">' +
'The nerve entrapment key for this region is under development.<br>' +
'It will differentiate muscles by their nerve entrapment signatures.<br><br>' +
'<small>Refer to individual muscle pages for entrapment detail in the meantime.</small>' +
'</div>' +
'</div>' +
'</div>';
}
shell.appendChild( nerveSection );
/* Tab switching */
var tabPain = shell.querySelector( '#dt-tab-pain' );
var tabNerve = shell.querySelector( '#dt-tab-nerve' );
tabPain.addEventListener( 'click', function () {
tabPain.classList.add( 'active' );
tabNerve.classList.remove( 'active' );
algoSection.style.display = '';
nerveSection.style.display = 'none';
} );
tabNerve.addEventListener( 'click', function () {
tabNerve.classList.add( 'active' );
tabPain.classList.remove( 'active' );
algoSection.style.display = 'none';
nerveSection.style.display = '';
} );
/* 3 — Broad differential (shared) */
var div2 = document.createElement( 'div' );
div2.className = 'dt-divider';
div2.textContent = 'Low Probability — Broad Differential';
shell.appendChild( div2 );
var broadEl = document.createElement( 'div' );
shell.appendChild( broadEl );
var broadPanel = new BroadDiffPanel( broadEl, treeData.broad_differential || [] );
/* 4 — Unlock on red flags cleared */
var tree = null;
new RedFlagPanel( panelEl, treeData.redflags, function () {
algoSection.classList.remove( 'locked' );
algoSection.classList.add( 'unlocked' );
var body = document.getElementById( 'dt-algo-body' );
body.innerHTML = '';
if ( !tree ) tree = new DiagnosticTree( body, treeData );
broadPanel.unlock();
} );
}
/* ══════════════════════════════════════════════════════
API LOADER — fetch JSON from wiki page
══════════════════════════════════════════════════════ */
function loadPage( pageName, callback ) {
var api = new mw.Api();
api.get( {
action: 'query',
titles: pageName,
prop: 'revisions',
rvprop: 'content',
rvslots: 'main',
format: 'json'
} ).done( function ( data ) {
var pages = data.query.pages;
var pageId = Object.keys( pages )[ 0 ];
if ( pageId === '-1' ) { callback( null, 'Page not found: ' + pageName ); return; }
var content = pages[ pageId ].revisions[ 0 ].slots.main[ '*' ];
/* Strip any wiki markup after the closing } (e.g. category tags) */
var jsonEnd = content.lastIndexOf( '}' );
try {
callback( JSON.parse( content.substring( 0, jsonEnd + 1 ) ), null );
} catch ( e ) {
callback( null, 'Invalid JSON in ' + pageName + ': ' + e.message );
}
} ).fail( function () {
callback( null, 'API request failed for: ' + pageName );
} );
}
function bootHost( hostEl ) {
var treePage = hostEl.getAttribute( 'data-tree-page' );
if ( !treePage ) return;
hostEl.innerHTML = '<div style="padding:1em;font-family:monospace;font-size:0.8em;color:#999">Loading...</div>';
loadPage( treePage, function ( treeData, err ) {
hostEl.innerHTML = '';
if ( err ) {
hostEl.innerHTML = '<div class="dt-error">' + err + '</div>';
return;
}
bootShell( hostEl, treeData );
} );
}
/* ══════════════════════════════════════════════════════
MEDIAWIKI ENTRY POINT
══════════════════════════════════════════════════════ */
function init() {
document.querySelectorAll( '.diagnostic-tree-host' ).forEach( function ( el ) {
bootHost( el );
} );
}
if ( typeof mw !== 'undefined' ) {
mw.hook( 'wikipage.content' ).add( init );
}
}() );