mirror of
https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit.git
synced 2025-06-14 16:19:59 +00:00
1338 lines
78 KiB
HTML
1338 lines
78 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block content %}
|
|
<div class="px-4 py-5 my-5 text-center">
|
|
<img class="d-block mx-auto mb-4" src="{{ url_for('static', filename='FHIRFLARE.png') }}" alt="FHIRFLARE Ig Toolkit" width="192" height="192">
|
|
<h1 class="display-5 fw-bold text-body-emphasis">{{ title }}</h1>
|
|
<div class="col-lg-6 mx-auto">
|
|
<p class="lead mb-4">
|
|
View details of the processed FHIR Implementation Guide.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="container mt-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h2><i class="bi bi-info-circle me-2"></i>{{ title }}</h2>
|
|
<a href="{{ url_for('view_igs') }}" class="btn btn-secondary"><i class="bi bi-arrow-left"></i> Back to Package List</a>
|
|
</div>
|
|
|
|
{% if processed_ig %}
|
|
<div class="card">
|
|
<div class="card-header">Package Details</div>
|
|
<div class="card-body">
|
|
<dl class="row">
|
|
<dt class="col-sm-3">Package Name</dt>
|
|
<dd class="col-sm-9"><code>{{ processed_ig.package_name }}</code></dd>
|
|
<dt class="col-sm-3">Package Version</dt>
|
|
<dd class="col-sm-9">{{ processed_ig.version }}</dd>
|
|
<dt class="col-sm-3">Processed At</dt>
|
|
<dd class="col-sm-9">{{ processed_ig.processed_date.strftime('%Y-%m-%d %H:%M:%S UTC') }}</dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
|
|
{% if config.DISPLAY_PROFILE_RELATIONSHIPS %}
|
|
<div class="card mt-4">
|
|
<div class="card-header">Profile Relationships</div>
|
|
<div class="card-body">
|
|
<h6>Complies With</h6>
|
|
{% if complies_with_profiles %}
|
|
<ul>
|
|
{% for profile in complies_with_profiles %}
|
|
<li>{{ profile }}</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% else %}
|
|
<p class="text-muted"><em>No profiles declared as compatible.</em></p>
|
|
{% endif %}
|
|
|
|
<h6>Required Dependent Profiles (Must Also Validate Against)</h6>
|
|
{% if imposed_profiles %}
|
|
<ul>
|
|
{% for profile in imposed_profiles %}
|
|
<li>{{ profile }}</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% else %}
|
|
<p class="text-muted"><em>No imposed profiles.</em></p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="card mt-4">
|
|
<div class="card-header">Resource Types Found / Defined</div>
|
|
<div class="card-body">
|
|
{% if profile_list or base_list %}
|
|
<p class="mb-2">
|
|
<small>
|
|
<span class="badge bg-warning text-dark border me-1" style="font-size: 0.8em;" title="Profile contains Must Support elements">MS</span> = Contains Must Support Elements<br>
|
|
<span class="badge bg-info text-dark border me-1" style="font-size: 0.8em;" title="An optional Extension which contains Must Support sub-elements (e.g., value[x], type)">Optional MS Ext</span> = Optional Extension with Must Support Sub-Elements
|
|
</small>
|
|
</p>
|
|
{% if profile_list %}
|
|
<p class="mb-2"><small><span class="badge bg-primary text-light border me-1">Examples</span> = Examples will be displayed when selecting profile Types if contained in the IG</small></p>
|
|
<h6>Profiles Defined ({{ profile_list|length }}):</h6>
|
|
<div class="d-flex flex-wrap gap-1 mb-3 resource-type-list">
|
|
{% for type_info in profile_list %}
|
|
<a href="#" class="resource-type-link text-decoration-none"
|
|
data-package-name="{{ processed_ig.package_name }}"
|
|
data-package-version="{{ processed_ig.version }}"
|
|
data-resource-type="{{ type_info.name }}"
|
|
aria-label="View structure for {{ type_info.name }}">
|
|
{% if type_info.must_support %}
|
|
{% if optional_usage_elements.get(type_info.name) %}
|
|
<span class="badge bg-info text-dark border" title="Optional Extension with Must Support sub-elements">{{ type_info.name }}</span>
|
|
{% else %}
|
|
<span class="badge bg-warning text-dark border" title="Contains Must Support elements">{{ type_info.name }}</span>
|
|
{% endif %}
|
|
{% else %}
|
|
<span class="badge bg-light text-dark border">{{ type_info.name }}</span>
|
|
{% endif %}
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<p class="text-muted"><em>No profiles defined.</em></p>
|
|
{% endif %}
|
|
{% if base_list %}
|
|
<h6>Base Resource Types Referenced ({{ base_list|length }}):</h6>
|
|
<div class="d-flex flex-wrap gap-1 resource-type-list">
|
|
{% for type_info in base_list %}
|
|
<a href="#" class="resource-type-link text-decoration-none"
|
|
data-package-name="{{ processed_ig.package_name }}"
|
|
data-package-version="{{ processed_ig.version }}"
|
|
data-resource-type="{{ type_info.name }}"
|
|
aria-label="View structure for {{ type_info.name }}">
|
|
{% if type_info.must_support %}
|
|
<span class="badge bg-warning text-dark border" title="Contains Must Support elements">{{ type_info.name }}</span>
|
|
{% else %}
|
|
<span class="badge bg-light text-dark border">{{ type_info.name }}</span>
|
|
{% endif %}
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<p class="text-muted"><em>No base resource types referenced.</em></p>
|
|
{% endif %}
|
|
{% else %}
|
|
<p class="text-muted"><em>No resource type information extracted or stored.</em></p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div id="structure-display-wrapper" class="mt-4" style="display: none;">
|
|
<div class="card">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<span>Structure Definition for: <code id="structure-title"></code></span>
|
|
<button type="button" class="btn-close" aria-label="Close" onclick="document.getElementById('structure-display-wrapper').style.display='none'; document.getElementById('example-display-wrapper').style.display='none'; document.getElementById('raw-structure-wrapper').style.display='none';"></button>
|
|
</div>
|
|
<div class="card-body">
|
|
<div id="structure-loading" class="text-center py-3" style="display: none;">
|
|
<div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div>
|
|
</div>
|
|
<div id="structure-fallback-message" class="alert alert-info" style="display: none;"></div>
|
|
<ul class="nav nav-tabs" id="structureTabs" role="tablist">
|
|
<li class="nav-item">
|
|
<a class="nav-link active" id="differential-tab" data-bs-toggle="tab" href="#differential" role="tab" aria-controls="differential" aria-selected="true">Differential</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" id="snapshot-tab" data-bs-toggle="tab" href="#snapshot" role="tab" aria-controls="snapshot" aria-selected="false">Snapshot</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" id="must-support-tab" data-bs-toggle="tab" href="#must-support" role="tab" aria-controls="must-support" aria-selected="false">Must Support</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" id="key-elements-tab" data-bs-toggle="tab" href="#key-elements" role="tab" aria-controls="key-elements" aria-selected="false">Key Elements</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" id="constraints-tab" data-bs-toggle="tab" href="#constraints-tab-pane" role="tab">Constraints</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" id="terminology-tab" data-bs-toggle="tab" href="#terminology-tab-pane" role="tab">Terminology</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" id="searchparams-tab" data-bs-toggle="tab" href="#searchparams-tab-pane" role="tab">Search Params</a>
|
|
</li>
|
|
</ul>
|
|
<div class="tab-content mt-3">
|
|
<div class="tab-pane fade show active" id="differential" role="tabpanel" aria-labelledby="differential-tab">
|
|
<div class="row gx-2 small fw-bold border-bottom mb-1 d-none d-md-flex">
|
|
<div class="col-lg-4 col-md-3">Path</div>
|
|
<div class="col-lg-1 col-md-1 text-center">Card.</div>
|
|
<div class="col-lg-3 col-md-3">Type(s)</div>
|
|
<div class="col-lg-1 col-md-1 text-center">Status</div>
|
|
<div class="col-lg-3 col-md-4">Description</div>
|
|
</div>
|
|
<ul id="structure-tree-differential" class="list-group list-group-flush structure-tree-root"></ul>
|
|
</div>
|
|
<div class="tab-pane fade" id="snapshot" role="tabpanel" aria-labelledby="snapshot-tab">
|
|
<div class="row gx-2 small fw-bold border-bottom mb-1 d-none d-md-flex">
|
|
<div class="col-lg-4 col-md-3">Path</div>
|
|
<div class="col-lg-1 col-md-1 text-center">Card.</div>
|
|
<div class="col-lg-3 col-md-3">Type(s)</div>
|
|
<div class="col-lg-1 col-md-1 text-center">Status</div>
|
|
<div class="col-lg-3 col-md-4">Description</div>
|
|
</div>
|
|
<ul id="structure-tree-snapshot" class="list-group list-group-flush structure-tree-root"></ul>
|
|
</div>
|
|
<div class="tab-pane fade" id="must-support" role="tabpanel" aria-labelledby="must-support-tab">
|
|
<div class="row gx-2 small fw-bold border-bottom mb-1 d-none d-md-flex">
|
|
<div class="col-lg-4 col-md-3">Path</div>
|
|
<div class="col-lg-1 col-md-1 text-center">Card.</div>
|
|
<div class="col-lg-3 col-md-3">Type(s)</div>
|
|
<div class="col-lg-1 col-md-1 text-center">Status</div>
|
|
<div class="col-lg-3 col-md-4">Description</div>
|
|
</div>
|
|
<ul id="structure-tree-must-support" class="list-group list-group-flush structure-tree-root"></ul>
|
|
</div>
|
|
<div class="tab-pane fade" id="key-elements" role="tabpanel" aria-labelledby="key-elements-tab">
|
|
<div class="row gx-2 small fw-bold border-bottom mb-1 d-none d-md-flex">
|
|
<div class="col-lg-4 col-md-3">Path</div>
|
|
<div class="col-lg-1 col-md-1 text-center">Card.</div>
|
|
<div class="col-lg-3 col-md-3">Type(s)</div>
|
|
<div class="col-lg-1 col-md-1 text-center">Status</div>
|
|
<div class="col-lg-3 col-md-4">Description</div>
|
|
</div>
|
|
<ul id="structure-tree-key-elements" class="list-group list-group-flush structure-tree-root"></ul>
|
|
</div>
|
|
<div class="tab-pane fade" id="constraints-tab-pane" role="tabpanel" aria-labelledby="constraints-tab">
|
|
<div class="mt-3 table-responsive">
|
|
<p>Constraints defined in this profile:</p>
|
|
<table class="table table-sm table-bordered table-striped" id="constraints-table">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>Key</th>
|
|
<th>Severity</th>
|
|
<th>Path</th>
|
|
<th>Description</th>
|
|
<th>Expression (FHIRPath)</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr><td colspan="5">No constraints found or data not loaded.</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div class="tab-pane fade" id="terminology-tab-pane" role="tabpanel" aria-labelledby="terminology-tab">
|
|
<div class="mt-3 table-responsive">
|
|
<p>Terminology bindings defined in this profile:</p>
|
|
<table class="table table-sm table-bordered table-striped" id="bindings-table">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>Path</th>
|
|
<th>Strength</th>
|
|
<th>ValueSet</th>
|
|
<th>Description</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr><td colspan="4">No bindings found or data not loaded.</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div class="tab-pane fade" id="searchparams-tab-pane" role="tabpanel" aria-labelledby="searchparams-tab">
|
|
<div class="mt-3">
|
|
<p>Search parameters associated with this profile:</p>
|
|
<div class="alert alert-warning" role="alert">
|
|
<strong>Note:</strong> SearchParameter resources. This functionality is NEWLY implemented, please report any bugs)
|
|
</div>
|
|
<table class="table table-sm table-bordered table-striped" id="searchparams-table">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>Parameter</th>
|
|
<th>Type</th>
|
|
<th>Conformance</th>
|
|
<th>Requirements / Description</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr><td colspan="4">Search Parameter data not available. Requires backend implementation.</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="raw-structure-wrapper" class="mt-4" style="display: none;">
|
|
<div class="card">
|
|
<div class="card-header d-flex justify-content-between align-itemsitum-center">
|
|
<span>Raw Structure Definition for <code id="raw-structure-title"></code></span>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary btn-copy" id="copy-raw-def-button"
|
|
data-bs-toggle="tooltip" data-bs-placement="top" title="Copy Raw Definition JSON">
|
|
<i class="bi bi-clipboard" aria-hidden="true"></i>
|
|
<span class="visually-hidden">Copy Raw Definition JSON to clipboard</span>
|
|
</button>
|
|
</div>
|
|
<div class="card-body">
|
|
<div id="raw-structure-loading" class="text-center py-3" style="display: none;">
|
|
<div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div>
|
|
</div>
|
|
<pre><code id="raw-structure-content" class="p-2 d-block border bg-light language-json" style="max-height: 400px; overflow-y: auto;"></code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="example-display-wrapper" class="mt-4" style="display: none;">
|
|
<div class="card">
|
|
<div class="card-header">Examples for <code id="example-resource-type-title"></code></div>
|
|
<div class="card-body">
|
|
<div class="mb-3" id="example-selector-wrapper" style="display: none;">
|
|
<label for="example-select" class="form-label">Select Example:</label>
|
|
<select class="form-select form-select-sm" id="example-select"><option selected value="">-- Select --</option></select>
|
|
</div>
|
|
<div id="example-loading" class="text-center py-3" style="display: none;">
|
|
<div class="spinner-border spinner-border-sm text-secondary" role="status"><span class="visually-hidden">Loading...</span></div>
|
|
</div>
|
|
<div id="example-content-wrapper" style="display: none;">
|
|
<div class="form-check mb-2">
|
|
<input type="checkbox" class="form-check-input" id="includeNarrative" checked>
|
|
<label class="form-check-label" for="includeNarrative">Include Narrative</label>
|
|
</div>
|
|
<h6 id="example-filename" class="mt-2 small text-muted"></h6>
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="d-flex justify-content-between align-items-center mb-1">
|
|
<h6>Raw Content</h6>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary btn-copy" id="copy-raw-ex-button"
|
|
data-bs-toggle="tooltip" data-bs-placement="top" title="Copy Raw Content">
|
|
<i class="bi bi-clipboard" aria-hidden="true"></i>
|
|
<span class="visually-hidden">Copy Raw Content to clipboard</span>
|
|
</button>
|
|
</div>
|
|
<pre><code id="example-content-raw" class="p-2 d-block border bg-light language-json" style="max-height: 400px; overflow-y: auto; white-space: pre;"></code></pre>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="d-flex justify-content-between align-items-center mb-1">
|
|
<h6>Pretty-Printed JSON</h6>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary btn-copy" id="copy-pretty-json-button"
|
|
data-bs-toggle="tooltip" data-bs-placement="top" title="Copy JSON">
|
|
<i class="bi bi-clipboard" aria-hidden="true"></i>
|
|
<span class="visually-hidden">Copy JSON example to clipboard</span>
|
|
</button>
|
|
</div>
|
|
<pre><code id="example-content-json" class="p-2 d-block border bg-light language-json" style="max-height: 400px; overflow-y: auto;"></code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<div class="alert alert-warning" role="alert">Processed IG details not available.</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
|
|
|
|
<script>const examplesData = {{ examples_by_type | tojson | safe }};</script>
|
|
|
|
<script>
|
|
// --- START OF COMPLETE SCRIPT BLOCK ---
|
|
|
|
// --- Global Variables (Accessed by DOMContentLoaded) ---
|
|
let structureDisplayWrapper, structureTitle, structureLoading, structureFallbackMessage;
|
|
let structureTreeDifferential, structureTreeSnapshot, structureTreeMustSupport, structureTreeKeyElements;
|
|
let exampleDisplayWrapper, exampleSelectorWrapper, exampleSelect, exampleResourceTypeTitle;
|
|
let exampleLoading, exampleContentWrapper, exampleFilename, exampleContentRaw, exampleContentJson;
|
|
let rawStructureWrapper, rawStructureTitle, rawStructureContent, rawStructureLoading;
|
|
let copyRawDefButton, copyRawExButton, copyPrettyJsonButton;
|
|
let copyRawDefTooltipInstance = null;
|
|
let copyRawExTooltipInstance = null;
|
|
let copyPrettyJsonTooltipInstance = null;
|
|
let structureDataCache = {}; // Cache for fetched structure data
|
|
let includeNarrativeCheckbox; // New global for narrative toggle
|
|
|
|
// --- Global Helper Functions ---
|
|
function parseFhirPath(path) {
|
|
if (!path || typeof path !== 'string') return [];
|
|
// Split by dot, but look ahead for '[' to handle paths like Observation.component[slice].code
|
|
const parts = path.split(/\.(?![^\[]*\])|(?=\[)/) // Split on dots NOT inside brackets, or before opening bracket
|
|
.map(part => part.replace(/\[x\]/g, '')); // Remove [x]
|
|
return parts.filter(part => part); // Remove empty strings
|
|
}
|
|
|
|
function escapeHtmlAttr(str) {
|
|
if (!str) return '';
|
|
return str
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
// Helper to get consistent node key
|
|
function getNodeKey(element) {
|
|
if (!element) return null;
|
|
// Use id if it looks like a structured path (contains '.'), otherwise default to path
|
|
const baseKey = (element.id && element.id.includes('.')) ? element.id : element.path;
|
|
if (!baseKey) {
|
|
console.warn("Cannot generate key: element missing structured id and path", element);
|
|
return `no-key-${Math.random()}`;
|
|
}
|
|
// Append sliceName if present to ensure uniqueness for slices
|
|
return element.sliceName ? `${baseKey}::slice::${element.sliceName}` : baseKey;
|
|
}
|
|
|
|
// Helper function to initialize a node
|
|
function createNode(path, name, parentPathKey, element = null) {
|
|
// Determine flags based on element data passed from backend
|
|
const isInDifferential = element?.isInDifferential || false;
|
|
const mustSupport = element?.mustSupport || false;
|
|
|
|
return {
|
|
path: path,
|
|
name: name,
|
|
parentPathKey: parentPathKey, // Store the KEY of the parent for easier lookup
|
|
element: element,
|
|
children: [],
|
|
isInDifferential: isInDifferential, // Store flag for direct access
|
|
mustSupport: mustSupport // Store flag for direct access
|
|
// containsMustSupportSlice will be added later if applicable
|
|
};
|
|
}
|
|
|
|
// Refined Filter for Key Elements view
|
|
function filterKeyElements(node) {
|
|
if (!node || !node.element) return false; // Node must exist and have element data
|
|
const el = node.element;
|
|
|
|
// Criteria from definition:
|
|
// - Marked as mustSupport
|
|
if (el.mustSupport) return true;
|
|
// - Listed in differential (flag set by backend)
|
|
if (node.isInDifferential) return true; // Check the flag on the *node*
|
|
// - Mandatory (min cardinality > 0)
|
|
if (el.min !== undefined && el.min > 0) return true;
|
|
// - Max cardinality constrained (max defined and not '*')
|
|
// (We include any constraint, e.g., max=1 or max=0)
|
|
if (el.max !== undefined && el.max !== '*') return true;
|
|
// - Implicated in a constraint (element has constraints)
|
|
if (el.constraint && Array.isArray(el.constraint) && el.constraint.length > 0) return true;
|
|
// - Introduces slicing
|
|
if (el.slicing) return true;
|
|
// - Is a slice itself
|
|
if (el.sliceName) return true;
|
|
// - Is a modifier element
|
|
if (el.isModifier) return true;
|
|
|
|
return false; // Default to false if no criteria met
|
|
}
|
|
|
|
// --- Helper Function to Propagate MS Flag to Slicing Entries V2 ---
|
|
function flagSlicingEntriesWithMSSlices(node) {
|
|
if (!node) return false; // Base case for recursion
|
|
|
|
let containedMSSliceInChildren = false; // Flag for MS anywhere in the descendant tree
|
|
let directChildIsMSSlice = false; // Flag for MS in a direct child slice
|
|
|
|
if (node.children && node.children.length > 0) {
|
|
// Check direct children first for being MS slices
|
|
directChildIsMSSlice = node.children.some(child => child.element?.sliceName && child.mustSupport);
|
|
|
|
// Recursively check descendants AND collect whether any descendant branch contains an MS slice
|
|
node.children.forEach(child => {
|
|
// If the recursive call on ANY child returns true, it means an MS slice exists somewhere below that child
|
|
if (flagSlicingEntriesWithMSSlices(child)) {
|
|
containedMSSliceInChildren = true;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Determine if this node ITSELF is a must-support slice
|
|
const isMustSupportSlice = node.element?.sliceName && node.mustSupport;
|
|
|
|
// Flag THIS node if it defines slicing AND (a direct child is an MS slice OR a deeper descendant is an MS slice)
|
|
if (node.element?.slicing && (directChildIsMSSlice || containedMSSliceInChildren)) {
|
|
node.containsMustSupportSlice = true;
|
|
}
|
|
|
|
// Return true if THIS node is an MS slice OR if it was flagged as containing one below it
|
|
// This ensures the status propagates up correctly.
|
|
return isMustSupportSlice || node.containsMustSupportSlice === true;
|
|
} // End flagSlicingEntriesWithMSSlices V2
|
|
|
|
// --- Build Tree Data - V13 (Correct Parent Finding for Nested Slices) ---
|
|
function buildTreeData(elements, view, mustSupportPaths) {
|
|
const treeData = [];
|
|
const nodeMap = new Map(); // Map uses getNodeKey(element) as key
|
|
|
|
if (!elements || elements.length === 0) {
|
|
console.warn(`No elements provided for view '${view}'`);
|
|
return treeData;
|
|
}
|
|
|
|
// --- PRE-SORT elements (Keep V12 sorting) ---
|
|
try {
|
|
elements.sort((a, b) => {
|
|
const pathA = a.path || '';
|
|
const pathB = b.path || '';
|
|
const depthA = pathA.split('.').length;
|
|
const depthB = pathB.split('.').length;
|
|
if (depthA !== depthB) return depthA - depthB;
|
|
if (pathA !== pathB) return pathA.localeCompare(pathB);
|
|
const sliceA = a.sliceName || '';
|
|
const sliceB = b.sliceName || '';
|
|
return sliceA.localeCompare(sliceB);
|
|
});
|
|
} catch (e) {
|
|
console.error("Error sorting elements:", e, elements);
|
|
}
|
|
// --- END PRE-SORT ---
|
|
|
|
// --- Create Root Node (Keep V12 logic) ---
|
|
const rootElementData = elements.find(el => el.path && !el.path.includes('.'));
|
|
let rootNode;
|
|
if (rootElementData) {
|
|
const rootNodeKey = getNodeKey(rootElementData);
|
|
if (rootNodeKey) {
|
|
rootNode = createNode(rootElementData.path, rootElementData.path, 'Root', rootElementData);
|
|
treeData.push(rootNode);
|
|
nodeMap.set(rootNodeKey, rootNode);
|
|
} else {
|
|
console.error("Could not generate key for root element:", rootElementData);
|
|
return treeData;
|
|
}
|
|
} else {
|
|
console.error(`Cannot find root element in provided elements for view '${view}'. Cannot build tree.`);
|
|
return treeData;
|
|
}
|
|
|
|
const mustSupportPathsSet = mustSupportPaths instanceof Set ? mustSupportPaths : new Set(mustSupportPaths || []);
|
|
|
|
// --- Build Tree Structure ---
|
|
elements.forEach((element, index) => {
|
|
const path = element.path;
|
|
const id = element.id;
|
|
const nodeKey = getNodeKey(element);
|
|
const isSlice = !!element.sliceName;
|
|
|
|
if (!path || !nodeKey) { console.warn(`Skipping element ${index} with missing path/key`, element); return; }
|
|
|
|
// Root element handling (Keep V12 logic)
|
|
if (nodeKey === getNodeKey(rootElementData)) {
|
|
rootNode.isInDifferential = element.isInDifferential || rootNode.isInDifferential;
|
|
rootNode.mustSupport = element.mustSupport || rootNode.mustSupport || mustSupportPathsSet.has(path) || (id && mustSupportPathsSet.has(id)) || mustSupportPathsSet.has(nodeKey);
|
|
if (element.isInDifferential && !rootNode.element.isInDifferential) { rootNode.element = element; }
|
|
return;
|
|
}
|
|
|
|
// Set flags (Keep V12 logic)
|
|
element.isInDifferential = element.isInDifferential || false;
|
|
element.mustSupport = element.mustSupport || mustSupportPathsSet.has(path) || (id && mustSupportPathsSet.has(id)) || mustSupportPathsSet.has(nodeKey);
|
|
|
|
// --- Parent Finding Logic (V13) ---
|
|
let parentNodeKey = getNodeKey(rootElementData); // Default to root
|
|
let parentElement = rootElementData;
|
|
let parentFound = false;
|
|
|
|
// Check 1: Direct Slice Definition (e.g., component:DiastolicBP or coding:DBPCode)
|
|
if (isSlice) {
|
|
let expectedParentId = null;
|
|
let expectedParentPath = null;
|
|
|
|
// Derive the expected parent ID/Path from the slice's ID or Path
|
|
if (id && id.includes('.')) {
|
|
// Try deriving from ID first (more specific)
|
|
const idParts = id.split(':');
|
|
if (idParts.length > 1) {
|
|
const parentIdGuess = idParts.slice(0, -1).join(':');
|
|
if (parentIdGuess.includes('.')) {
|
|
expectedParentId = parentIdGuess;
|
|
}
|
|
}
|
|
// Fallback: if ID didn't work well, try deriving from path
|
|
if (!expectedParentId && path && path.includes('.')) {
|
|
expectedParentPath = path; // The base path is the parent slicing entry path
|
|
}
|
|
} else if (path && path.includes('.')) {
|
|
// If no complex ID, use the path
|
|
expectedParentPath = path;
|
|
}
|
|
|
|
// Now, find the slicing entry element using the derived parent identifiers
|
|
let slicingEntryElement = null;
|
|
if (expectedParentId) {
|
|
// Prefer ID match
|
|
slicingEntryElement = elements.find(el => el.id === expectedParentId && el.slicing);
|
|
}
|
|
if (!slicingEntryElement && expectedParentPath) {
|
|
// Fallback to path match if ID didn't work or wasn't derived
|
|
slicingEntryElement = elements.find(el => el.path === expectedParentPath && !el.sliceName && el.slicing);
|
|
}
|
|
|
|
if (slicingEntryElement) {
|
|
parentElement = slicingEntryElement;
|
|
parentNodeKey = getNodeKey(parentElement);
|
|
parentFound = true;
|
|
} else {
|
|
console.warn(`Slicing entry element NOT FOUND for expected parent (ID: ${expectedParentId}, Path: ${expectedParentPath}) needed by slice '${nodeKey}'. Assigning root.`);
|
|
parentElement = rootElementData;
|
|
parentNodeKey = getNodeKey(parentElement);
|
|
parentFound = true; // Consider parent determined (fallback)
|
|
}
|
|
}
|
|
// Check 2: Descendant of a Slice (using ID structure) (Keep V12 logic)
|
|
else if (id && id.includes(':') && id.includes('.')) {
|
|
const idParts = id.split('.');
|
|
let potentialSliceParentId = null;
|
|
for (let i = idParts.length - 2; i >= 0; i--) {
|
|
const tempId = idParts.slice(0, i + 1).join('.');
|
|
if (tempId.includes(':')) { potentialSliceParentId = tempId; break; }
|
|
}
|
|
if (potentialSliceParentId) {
|
|
const potentialParentKey = potentialSliceParentId;
|
|
if (nodeMap.has(potentialParentKey)) {
|
|
const potentialParentNode = nodeMap.get(potentialParentKey);
|
|
if (potentialParentNode?.element) {
|
|
parentNodeKey = potentialParentKey; parentElement = potentialParentNode.element; parentFound = true;
|
|
} else { console.warn(`Node found in map for key ${potentialParentKey} but its element was missing. Falling back.`); }
|
|
} else {
|
|
const sliceParentElement = elements.find(el => el.id === potentialSliceParentId);
|
|
if (sliceParentElement) {
|
|
parentNodeKey = getNodeKey(sliceParentElement); parentElement = sliceParentElement; parentFound = true;
|
|
} else { console.warn(`Slice parent element ID '${potentialSliceParentId}' not found for descendant '${id}'. Falling back.`); }
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check 3 & 4: Regular or Choice Descendant (Keep V12 logic)
|
|
if (!parentFound) {
|
|
const pathParts = parseFhirPath(path);
|
|
if (pathParts.length > 1) {
|
|
const parentPathString = pathParts.slice(0, -1).join('.');
|
|
let potentialParentNode = nodeMap.get(parentPathString);
|
|
if (!potentialParentNode && path.includes('[x]')) {
|
|
const originalPathParts = path.split('.');
|
|
const parentPathWithChoice = originalPathParts.slice(0, -1).join('.');
|
|
potentialParentNode = nodeMap.get(parentPathWithChoice);
|
|
}
|
|
if (potentialParentNode?.element) {
|
|
parentElement = potentialParentNode.element;
|
|
parentNodeKey = getNodeKey(parentElement);
|
|
parentFound = true;
|
|
} else {
|
|
parentElement = rootElementData; parentNodeKey = getNodeKey(parentElement);
|
|
parentFound = true;
|
|
}
|
|
} else {
|
|
parentElement = rootElementData; parentNodeKey = getNodeKey(parentElement);
|
|
parentFound = true;
|
|
}
|
|
}
|
|
// --- End Parent Finding Logic ---
|
|
|
|
// --- Node Creation / Update / Attachment (Keep V12 logic) ---
|
|
let parentNode = nodeMap.get(parentNodeKey);
|
|
if (!parentNode) {
|
|
console.error(`Parent node OBJECT NOT FOUND in map for resolved key: '${parentNodeKey}' (child: ${nodeKey}). Assigning child to root.`);
|
|
parentNode = rootNode;
|
|
parentNodeKey = getNodeKey(rootElementData);
|
|
}
|
|
|
|
if (nodeMap.has(nodeKey)) {
|
|
const existingNode = nodeMap.get(nodeKey);
|
|
existingNode.element = element;
|
|
existingNode.isInDifferential = existingNode.isInDifferential || element.isInDifferential;
|
|
existingNode.mustSupport = existingNode.mustSupport || element.mustSupport;
|
|
if (existingNode.parentPathKey && existingNode.parentPathKey !== parentNodeKey) {
|
|
console.warn(`[!] Ambiguous Parent: Node ${nodeKey} already exists with parent ${existingNode.parentPathKey}, but current logic suggests parent ${parentNodeKey}. Keeping original parent.`);
|
|
} else {
|
|
existingNode.parentPathKey = parentNodeKey;
|
|
}
|
|
} else {
|
|
const pathPartsForName = parseFhirPath(path);
|
|
const baseName = pathPartsForName.length > 0 ? pathPartsForName[pathPartsForName.length - 1] : path;
|
|
const name = element.sliceName ? `${baseName}:${element.sliceName}` : baseName;
|
|
const newNode = createNode(path, name, parentNodeKey, element);
|
|
nodeMap.set(nodeKey, newNode);
|
|
if (parentNode) {
|
|
if (!parentNode.children) parentNode.children = [];
|
|
parentNode.children.push(newNode);
|
|
} else {
|
|
console.error(`Cannot add child ${nodeKey} as parent node object is unexpectedly null.`);
|
|
}
|
|
}
|
|
}); // End elements.forEach
|
|
|
|
// --- Filtering Logic (Keep V12 logic) ---
|
|
if ((view === 'must-support' || view === 'key-elements' || view === 'differential') && treeData.length > 0 && treeData[0]) {
|
|
const filterNodesRecursively = (nodes, filterFnParam) => {
|
|
if (!nodes) return [];
|
|
return nodes.map(node => {
|
|
if (!node) return null;
|
|
const filteredChildren = filterNodesRecursively(node.children || [], filterFnParam);
|
|
const filterMatch = filterFnParam(node);
|
|
if (filterMatch || filteredChildren.length > 0) {
|
|
node.children = filteredChildren;
|
|
return node;
|
|
}
|
|
return null;
|
|
}).filter(node => node !== null);
|
|
};
|
|
let filterFunction;
|
|
if (view === 'must-support') { filterFunction = (node) => node.mustSupport || node.containsMustSupportSlice; }
|
|
else if (view === 'key-elements') { filterFunction = filterKeyElements; }
|
|
else { filterFunction = (node) => node.isInDifferential; } // Differential view
|
|
|
|
if (treeData[0]) {
|
|
treeData[0].children = filterNodesRecursively(treeData[0].children || [], filterFunction);
|
|
if (!filterFunction(treeData[0]) && (!treeData[0].children || treeData[0].children.length === 0)) {
|
|
if (view !== 'snapshot') {
|
|
console.log(`Root node ${treeData[0].path} does not meet criteria for view '${view}' and has no matching children. Clearing tree.`);
|
|
return [];
|
|
}
|
|
}
|
|
}
|
|
} // End Filtering Logic
|
|
|
|
// --- Post-processing step for MS highlighting (Keep V12 logic) ---
|
|
if (treeData.length > 0 && treeData[0]) { flagSlicingEntriesWithMSSlices(treeData[0]); }
|
|
|
|
return treeData;
|
|
} // End buildTreeData V13
|
|
|
|
// --- Render Node as List Item - V19 (Fixes isSlice error, keeps V18 logic) ---
|
|
function renderNodeAsLi(node, mustSupportPathsSet, resourceType, level = 0, view = 'snapshot') {
|
|
if (!node || !node.path || !node.name) { console.warn(`Skipping render for invalid node:`, node); return ''; }
|
|
const el = node.element || {};
|
|
const path = el.path || node.path || `node-${level}-${Math.random()}`;
|
|
const elementId = el.id || null; // Renamed variable
|
|
const nodeKey = getNodeKey(el);
|
|
|
|
const sliceName = el.sliceName || null;
|
|
// Define isSlice based on sliceName
|
|
const isSlice = sliceName ? true : false;
|
|
const min = el.min !== undefined ? el.min : '';
|
|
const max = el.max !== undefined ? el.max : '*';
|
|
const short = el.short || '';
|
|
const definition = el.definition || short || '';
|
|
const isChoiceElement = (el.path && el.path.endsWith('[x]'));
|
|
const definesSlicing = el.slicing ? true : false;
|
|
|
|
// Must Support Logic & Icon (V15 logic)
|
|
let isMustSupport = node.mustSupport || false;
|
|
mustSupportPathsSet = mustSupportPathsSet instanceof Set ? mustSupportPathsSet : new Set(mustSupportPathsSet || []);
|
|
let mustSupportTitle = isMustSupport ? 'Must Support' : '';
|
|
let fhirPathExpression = path;
|
|
if (sliceName && isMustSupport) {
|
|
mustSupportTitle = `Must Support (Slice: ${sliceName})`;
|
|
fhirPathExpression = `${path}[sliceName='${sliceName}']`;
|
|
}
|
|
const msIconClass = (isMustSupport || node.containsMustSupportSlice) ? 'text-warning' : '';
|
|
const mustSupportDisplay = (isMustSupport || node.containsMustSupportSlice) ? `<i class="bi bi-check-circle-fill ${msIconClass} ms-1" title="${escapeHtmlAttr(mustSupportTitle)}" data-bs-toggle="tooltip" data-fhirpath="${escapeHtmlAttr(fhirPathExpression)}"></i>` : '';
|
|
|
|
// Styling Logic (V15 logic)
|
|
let liClass = (isMustSupport || node.containsMustSupportSlice) ? 'list-group-item py-1 px-2 list-group-item-warning' : 'list-group-item py-1 px-2';
|
|
if (view === 'differential') {
|
|
if (!node.isInDifferential) { liClass += ' text-muted'; }
|
|
if (max === '0') { liClass += ' text-decoration-line-through'; }
|
|
}
|
|
|
|
const hasActualChildren = node.children && node.children.length > 0;
|
|
// Show toggle logic (V18 logic - reverted primitive filtering)
|
|
const showToggle = hasActualChildren || definesSlicing || (isChoiceElement && !definesSlicing);
|
|
|
|
// Collapse ID Generation (V13 logic)
|
|
let uniquePartBase = nodeKey || path;
|
|
if (!uniquePartBase || typeof uniquePartBase !== 'string') { uniquePartBase = `fallback-${level}-${Math.random()}`; }
|
|
const uniquePart = uniquePartBase.replace(/[^a-zA-Z0-9\-:_]/g, '-') + `-${view}-${level}`;
|
|
const collapseId = `collapse-${uniquePart}`;
|
|
|
|
const padding = level * 20;
|
|
const pathStyle = `padding-left: ${padding}px; white-space: nowrap;`;
|
|
|
|
// Type String Logic
|
|
let typeString = '<span class="text-muted">N/A</span>';
|
|
if (el.type && Array.isArray(el.type) && el.type.length > 0) {
|
|
typeString = el.type.map(t => {
|
|
let s = escapeHtmlAttr(t.code || 'Unknown');
|
|
let profiles = t.targetProfile || t.profile || [];
|
|
if (profiles.length > 0) {
|
|
const targetTypes = profiles.map(p => (p || '').split('/').pop()).filter(Boolean).join(', ');
|
|
if (targetTypes) { s += `(<span class="fst-italic" title="Profile(s): ${escapeHtmlAttr(profiles.join(', '))}">${escapeHtmlAttr(targetTypes)}</span>)`; }
|
|
}
|
|
return s;
|
|
}).join(' | ');
|
|
} else if (el.contentReference) { typeString = `<span class="text-info" title="Content Reference">Ref: ${escapeHtmlAttr(el.contentReference)}</span>`; }
|
|
else if (el.sliceName && path.endsWith('extension')) { typeString = '<span class="text-muted">Extension</span>'; }
|
|
|
|
// Children HTML / Choice Type Display (V18 logic - reverted primitive filtering)
|
|
let childrenOrChoiceHtml = '';
|
|
if (showToggle) {
|
|
childrenOrChoiceHtml += `<ul class="collapse list-group list-group-flush ms-3" id="${collapseId}">`;
|
|
// Render children if they exist OR if slicing defined
|
|
if (hasActualChildren || definesSlicing) {
|
|
const sortedChildren = (node.children || []).sort((a, b) => (a.name || '').localeCompare(b.name || ''));
|
|
sortedChildren.forEach(childNode => {
|
|
if (childNode) { childrenOrChoiceHtml += renderNodeAsLi(childNode, mustSupportPathsSet, resourceType, level + 1, view); }
|
|
});
|
|
}
|
|
// Render choice types ONLY if it's a choice element AND not sliced
|
|
else if (isChoiceElement && !definesSlicing && el.type && Array.isArray(el.type)) {
|
|
el.type.forEach(choiceType => {
|
|
const typeCode = choiceType.code || 'Unknown';
|
|
const baseName = (node.name || path.split('.').pop()).replace('[x]', '');
|
|
const choiceDisplayName = escapeHtmlAttr(baseName + typeCode.charAt(0).toUpperCase() + typeCode.slice(1));
|
|
const choiceTypeCode = escapeHtmlAttr(typeCode);
|
|
const targetProfiles = choiceType.targetProfile || choiceType.profile || [];
|
|
let profileDisplay = '';
|
|
if (targetProfiles.length > 0) {
|
|
const targetTypes = targetProfiles.map(p => (p || '').split('/').pop()).filter(Boolean).join(', ');
|
|
if (targetTypes) { profileDisplay = ` (<span class="fst-italic" title="Profile(s): ${escapeHtmlAttr(targetProfiles.join(', '))}">${escapeHtmlAttr(targetTypes)}</span>)`; }
|
|
}
|
|
const choiceTooltipAttr = definition ? `title="${escapeHtmlAttr(definition)}"` : '';
|
|
const choicePadding = padding + 20;
|
|
childrenOrChoiceHtml += `<li class="list-group-item py-1 border-0 choice-type-item" ${choiceTooltipAttr} ${definition ? 'data-bs-toggle="tooltip" data-bs-placement="top"' : ''}>` +
|
|
`<div class="row gx-2 align-items-center">` +
|
|
`<div class="col-lg-4 col-md-3 text-truncate" style="padding-left: ${choicePadding}px;"><code>${choiceDisplayName}</code></div>` +
|
|
`<div class="col-lg-1 col-md-1 text-center small"></div>` +
|
|
`<div class="col-lg-3 col-md-3 text-truncate small"><code>${choiceTypeCode}</code>${profileDisplay}</div>` +
|
|
`<div class="col-lg-1 col-md-1 text-center"></div>` +
|
|
`<div class="col-lg-3 col-md-4 text-truncate small">${escapeHtmlAttr(short)}</div>` +
|
|
`</div>` +
|
|
`</li>`;
|
|
});
|
|
}
|
|
childrenOrChoiceHtml += `</ul>`;
|
|
}
|
|
|
|
// Build list item HTML (Main row)
|
|
let itemHtml = `<li class="${liClass}" style="display: block;">`;
|
|
itemHtml += `<div class="row gx-2 align-items-center ${showToggle ? 'collapse-toggle' : ''}" ${showToggle ? `data-bs-toggle="collapse" data-bs-target="#${collapseId}" role="button" aria-expanded="false" aria-controls="${collapseId}" style="cursor: pointer;"` : ''}>`;
|
|
itemHtml += `<div class="col-lg-4 col-md-3 text-truncate" style="${pathStyle}">`;
|
|
itemHtml += `<span style="display: inline-block; width: 1.2em; text-align: center;">`;
|
|
if (showToggle) itemHtml += `<i class="bi bi-chevron-right small toggle-icon"></i>`;
|
|
itemHtml += `</span>`;
|
|
let displayName = escapeHtmlAttr(node.name);
|
|
if (definesSlicing && !isSlice) { displayName = `Slices for ${displayName}`; } // Use defined isSlice
|
|
const pathTitle = fhirPathExpression ? `${path} (FHIRPath: ${fhirPathExpression})` : path;
|
|
itemHtml += `<code class="fw-bold ms-1" title="${escapeHtmlAttr(pathTitle)}" data-bs-toggle="tooltip">${displayName}</code>`;
|
|
itemHtml += `</div>`;
|
|
const cardString = (min !== '' || max !== '') ? `${min}..${max}` : '';
|
|
itemHtml += `<div class="col-lg-1 col-md-1 text-center small"><code>${cardString}</code></div>`;
|
|
itemHtml += `<div class="col-lg-3 col-md-3 text-truncate small">${typeString}</div>`;
|
|
itemHtml += `<div class="col-lg-1 col-md-1 text-center">${mustSupportDisplay}</div>`;
|
|
const descriptionTooltipAttr = definition ? `title="${escapeHtmlAttr(definition)}"` : '';
|
|
itemHtml += `<div class="col-lg-3 col-md-4 text-truncate small" ${descriptionTooltipAttr} ${definition ? 'data-bs-toggle="tooltip" data-bs-placement="top"' : ''}>${escapeHtmlAttr(short)}</div>`;
|
|
itemHtml += `</div>`; // End row
|
|
itemHtml += childrenOrChoiceHtml;
|
|
itemHtml += `</li>`;
|
|
return itemHtml;
|
|
} // End renderNodeAsLi (V19)
|
|
|
|
// --- Function to Initialize Collapse Icon Listeners (Bootstrap Events) V3 - Explicit Cleanup ---
|
|
function initializeBootstrapCollapseListeners(containerElement) {
|
|
if (!containerElement) { return; }
|
|
const viewName = containerElement.id.replace('structure-tree-', '');
|
|
const collapseElements = containerElement.querySelectorAll('.collapse');
|
|
collapseElements.forEach((collapseEl, index) => {
|
|
const collapseId = collapseEl.id; if (!collapseId) { return; }
|
|
const triggerEl = containerElement.querySelector(`[data-bs-target="#${collapseId}"]`);
|
|
const icon = triggerEl?.querySelector('.toggle-icon');
|
|
if (!triggerEl) { console.warn(`Trigger not found for #${collapseId}`); }
|
|
if (collapseEl._handleShow && typeof collapseEl._handleShow === 'function') { collapseEl.removeEventListener('show.bs.collapse', collapseEl._handleShow); }
|
|
if (collapseEl._handleHide && typeof collapseEl._handleHide === 'function') { collapseEl.removeEventListener('hide.bs.collapse', collapseEl._handleHide); }
|
|
const handleShow = (event) => { if (event.target === collapseEl && icon) { icon.classList.replace('bi-chevron-right', 'bi-chevron-down'); } };
|
|
const handleHide = (event) => { if (event.target === collapseEl && icon) { icon.classList.replace('bi-chevron-down', 'bi-chevron-right'); } };
|
|
collapseEl._handleShow = handleShow; collapseEl._handleHide = handleHide;
|
|
collapseEl.addEventListener('show.bs.collapse', handleShow);
|
|
collapseEl.addEventListener('hide.bs.collapse', handleHide);
|
|
if (icon) { /* Set initial icon */ if (collapseEl.classList.contains('show')) { icon.classList.replace('bi-chevron-right', 'bi-chevron-down'); } else { icon.classList.replace('bi-chevron-down', 'bi-chevron-right'); } }
|
|
});
|
|
} // End initializeBootstrapCollapseListeners V3
|
|
|
|
// --- Render Structure Tree (Calls V13 buildTreeData, V19 renderNode, V3 initBSCollapse) ---
|
|
async function renderStructureTree(resourceType, view, snapshotElements, mustSupportPaths) { // Accepts snapshot elements
|
|
const treeContainer = document.getElementById(`structure-tree-${view}`);
|
|
if (!treeContainer) { console.error(`Tree container missing: ${view}`); return; }
|
|
treeContainer.innerHTML = '';
|
|
|
|
try {
|
|
// Build tree using V13 buildTreeData and snapshot data
|
|
const treeData = buildTreeData(snapshotElements, view, mustSupportPaths);
|
|
|
|
// Render HTML using V19 renderNodeAsLi
|
|
let html = '<p class="ms-2 mb-1"><small>...</small></p>'; // Legend
|
|
html += '<ul class="list-group list-group-flush structure-tree-root">';
|
|
if (treeData.length > 0 && treeData[0]) {
|
|
html += renderNodeAsLi(treeData[0], mustSupportPaths, resourceType, 0, view); // V19 Call
|
|
} else { html += `<li class="list-group-item"><em class="text-muted">No elements match...</em></li>`; }
|
|
html += '</ul>';
|
|
treeContainer.innerHTML = html;
|
|
|
|
// Init listeners/tooltips
|
|
initializeBootstrapCollapseListeners(treeContainer); // V3
|
|
const tooltipTriggerList = treeContainer.querySelectorAll('[data-bs-toggle="tooltip"]');
|
|
tooltipTriggerList.forEach(tooltipTriggerEl => {
|
|
const e = bootstrap.Tooltip.getInstance(tooltipTriggerEl);
|
|
if (e) { e.dispose(); }
|
|
if (tooltipTriggerEl.getAttribute('title') || tooltipTriggerEl.getAttribute('data-bs-original-title')) {
|
|
new bootstrap.Tooltip(tooltipTriggerEl);
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error(`Error rendering ${view} tree:`, error);
|
|
treeContainer.innerHTML = `<div class="alert alert-danger m-2">Error rendering ${view}: ${error.message}</div>`;
|
|
}
|
|
} // End renderStructureTree
|
|
|
|
// --- Fetch Structure Definition for Specific View ---
|
|
async function fetchStructure(resourceType, view, includeNarrative, raw = false) {
|
|
const cacheKey = `${resourceType}-${view}-${includeNarrative}-${raw}`;
|
|
if (structureDataCache[cacheKey]) {
|
|
try { return JSON.parse(JSON.stringify(structureDataCache[cacheKey])); } catch (e) { return structureDataCache[cacheKey]; }
|
|
}
|
|
const structureBaseUrl = "{{ url_for('get_structure') }}";
|
|
const params = new URLSearchParams({
|
|
package_name: "{{ processed_ig.package_name }}",
|
|
version: "{{ processed_ig.version }}",
|
|
resource_type: resourceType,
|
|
view: view,
|
|
include_narrative: includeNarrative,
|
|
raw: raw
|
|
});
|
|
const fetchUrl = `${structureBaseUrl}?${params.toString()}`;
|
|
try {
|
|
const response = await fetch(fetchUrl);
|
|
if (!response.ok) { throw new Error(`HTTP error ${response.status}`); }
|
|
const data = await response.json();
|
|
try { structureDataCache[cacheKey] = JSON.parse(JSON.stringify(data)); } catch(e) { structureDataCache[cacheKey] = data; }
|
|
return JSON.parse(JSON.stringify(data));
|
|
} catch (error) { console.error(`Error fetching ${raw ? 'raw' : view} structure:`, error); throw error; }
|
|
}
|
|
|
|
// --- Populate Structure for All Views - V5 (Add Search Param Logging, Include Narrative) ---
|
|
async function populateStructure(resourceType) {
|
|
if (structureTitle) structureTitle.textContent = `${resourceType} ({{ processed_ig.package_name }}#{{ processed_ig.version }})`;
|
|
if (rawStructureTitle) rawStructureTitle.textContent = `${resourceType} ({{ processed_ig.package_name }}#{{ processed_ig.version }})`;
|
|
if (structureLoading) structureLoading.style.display = 'block';
|
|
if (rawStructureLoading) rawStructureLoading.style.display = 'block';
|
|
if (structureDisplayWrapper) structureDisplayWrapper.style.display = 'block';
|
|
if (rawStructureWrapper) rawStructureWrapper.style.display = 'block';
|
|
if (structureFallbackMessage) { structureFallbackMessage.style.display = 'none'; structureFallbackMessage.textContent = ''; }
|
|
const views = ['differential', 'snapshot', 'must-support', 'key-elements'];
|
|
views.forEach(v => { const tc = document.getElementById(`structure-tree-${v}`); if(tc) tc.innerHTML = ''; });
|
|
const constraintsTableBody = document.getElementById('constraints-table')?.querySelector('tbody');
|
|
const bindingsTableBody = document.getElementById('bindings-table')?.querySelector('tbody');
|
|
const searchParamsTableBody = document.getElementById('searchparams-table')?.querySelector('tbody');
|
|
if (constraintsTableBody) constraintsTableBody.innerHTML = '<tr><td colspan="5" class="text-center text-muted fst-italic">Loading...</td></tr>';
|
|
if (bindingsTableBody) bindingsTableBody.innerHTML = '<tr><td colspan="4" class="text-center text-muted fst-italic">Loading...</td></tr>';
|
|
if (searchParamsTableBody) searchParamsTableBody.innerHTML = '<tr><td colspan="4" class="text-center text-muted fst-italic">Loading...</td></tr>';
|
|
if (rawStructureContent) rawStructureContent.textContent = 'Loading...';
|
|
try {
|
|
console.log(`PopulateStructure V4: Fetching structure and search params for ${resourceType}`);
|
|
const includeNarrative = includeNarrativeCheckbox ? includeNarrativeCheckbox.checked : true;
|
|
// Fetch processed data for UI tabs
|
|
const processedData = await fetchStructure(resourceType, 'snapshot', includeNarrative, false);
|
|
if (!processedData || !processedData.elements) { throw new Error(`Structure data or elements missing for ${resourceType}.`); }
|
|
const snapshotElementsCopy = JSON.parse(JSON.stringify(processedData.elements));
|
|
const mustSupportPaths = new Set(processedData.must_support_paths || []);
|
|
const searchParams = processedData.search_parameters || [];
|
|
const fallbackUsed = processedData.fallback_used || false;
|
|
const sourcePackage = processedData.source_package || 'FHIR core';
|
|
console.log('[populateStructure] Received search_parameters:', searchParams);
|
|
// Fetch raw StructureDefinition for raw view
|
|
const rawData = await fetchStructure(resourceType, 'snapshot', includeNarrative, true);
|
|
if (rawStructureContent) {
|
|
rawStructureContent.textContent = JSON.stringify(rawData, null, 4);
|
|
if (rawStructureContent.classList.contains('hljs')) { rawStructureContent.classList.remove('hljs'); }
|
|
rawStructureContent.dataset.highlighted = '';
|
|
hljs.highlightElement(rawStructureContent);
|
|
if (fallbackUsed && structureFallbackMessage) {
|
|
structureFallbackMessage.textContent = `Note: Displaying structure from fallback package: ${sourcePackage}`;
|
|
structureFallbackMessage.style.display = 'block';
|
|
}
|
|
} else { if (rawStructureContent) rawStructureContent.textContent = '(Raw content display element not found)'; }
|
|
for (const view of views) {
|
|
console.log(`PopulateStructure V4: Rendering view '${view}' for ${resourceType}`);
|
|
const treeContainer = document.getElementById(`structure-tree-${view}`);
|
|
if (!treeContainer) { console.error(`Container missing for ${view}`); continue; }
|
|
await renderStructureTree(resourceType, view, JSON.parse(JSON.stringify(snapshotElementsCopy)), mustSupportPaths);
|
|
}
|
|
renderConstraintsTable(processedData.elements);
|
|
renderBindingsTable(processedData.elements);
|
|
renderSearchParametersTable(searchParams);
|
|
} catch (error) {
|
|
console.error("Error populating structure:", error);
|
|
views.forEach(v => { const tc = document.getElementById(`structure-tree-${v}`); if(tc) tc.innerHTML = `<div class="alert alert-danger m-2">Error loading structure: ${error.message}</div>`; });
|
|
if (constraintsTableBody) constraintsTableBody.innerHTML = `<tr><td colspan="5" class="text-danger text-center">Error loading constraints: ${error.message}</td></tr>`;
|
|
if (bindingsTableBody) bindingsTableBody.innerHTML = `<tr><td colspan="4" class="text-danger text-center">Error loading bindings: ${error.message}</td></tr>`;
|
|
if (searchParamsTableBody) searchParamsTableBody.innerHTML = `<tr><td colspan="4" class="text-danger text-center">Error loading search params: ${error.message}</td></tr>`;
|
|
if (rawStructureContent) { rawStructureContent.textContent = `Error: ${error.message}`; }
|
|
} finally {
|
|
if (structureLoading) structureLoading.style.display = 'none';
|
|
if (rawStructureLoading) rawStructureLoading.style.display = 'none';
|
|
}
|
|
}// End populateStructure V5
|
|
|
|
// --- Populate Example Selector Dropdown ---
|
|
function populateExampleSelector(resourceOrProfileIdentifier) {
|
|
if (!exampleResourceTypeTitle || !exampleSelect || !exampleContentWrapper || !exampleFilename || !exampleContentRaw || !exampleContentJson || !exampleLoading || !exampleSelectorWrapper || !examplesData) { return; }
|
|
exampleResourceTypeTitle.textContent = resourceOrProfileIdentifier;
|
|
const availableExamples = examplesData[resourceOrProfileIdentifier] || [];
|
|
exampleSelect.innerHTML = '<option selected value="">-- Select an Example --</option>';
|
|
exampleContentWrapper.style.display = 'none'; exampleFilename.textContent = ''; exampleContentRaw.textContent = ''; exampleContentJson.textContent = ''; exampleLoading.style.display = 'none';
|
|
if (availableExamples.length > 0) {
|
|
availableExamples.forEach(filePath => { const f=filePath.split('/').pop(); const o=document.createElement('option'); o.value=filePath; o.textContent=f; exampleSelect.appendChild(o); });
|
|
exampleSelectorWrapper.style.display = 'block'; if (exampleDisplayWrapper) exampleDisplayWrapper.style.display = 'block';
|
|
} else { exampleSelectorWrapper.style.display = 'none'; if (exampleDisplayWrapper) exampleDisplayWrapper.style.display = 'none'; }
|
|
}
|
|
|
|
// --- Fetch and Display Selected Example ---
|
|
async function fetchAndDisplayExample(selectedFilePath) {
|
|
if (!exampleContentRaw || !exampleContentJson || !exampleFilename || !exampleContentWrapper || !exampleLoading) { return; }
|
|
exampleContentRaw.textContent = ''; exampleContentJson.textContent = ''; exampleFilename.textContent = ''; exampleContentWrapper.style.display = 'none'; exampleLoading.style.display = 'block';
|
|
const resetCopyButton = (button, tooltipInstance, originalTitle) => { if (tooltipInstance && button?.isConnected) { tooltipInstance.hide(); button.setAttribute('data-bs-original-title', originalTitle); button.querySelector('i')?.classList.replace('bi-check-lg', 'bi-clipboard'); } };
|
|
resetCopyButton(copyRawExButton, copyRawExTooltipInstance, 'Copy Raw Content'); resetCopyButton(copyPrettyJsonButton, copyPrettyJsonTooltipInstance, 'Copy JSON');
|
|
if (!selectedFilePath) { exampleLoading.style.display = 'none'; return; }
|
|
const exampleBaseUrl = "{{ url_for('get_example') }}";
|
|
const includeNarrative = includeNarrativeCheckbox ? includeNarrativeCheckbox.checked : true;
|
|
const exampleParams = new URLSearchParams({
|
|
package_name: "{{ processed_ig.package_name }}",
|
|
version: "{{ processed_ig.version }}", // Changed from package_version to version
|
|
filename: selectedFilePath,
|
|
include_narrative: includeNarrative
|
|
});
|
|
const exampleFetchUrl = `${exampleBaseUrl}?${exampleParams.toString()}`;
|
|
try {
|
|
const response = await fetch(exampleFetchUrl);
|
|
if (!response.ok) { throw new Error(`HTTP error ${response.status}`); }
|
|
const rawData = await response.text();
|
|
if(exampleFilename) exampleFilename.textContent = `Source: ${selectedFilePath.split('/').pop()}`;
|
|
if(exampleContentRaw) exampleContentRaw.textContent = rawData;
|
|
let prettyJson = '(Not valid JSON)';
|
|
try { prettyJson = JSON.stringify(JSON.parse(rawData), null, 4); } catch (e) { prettyJson = rawData.startsWith('<') ? '(Content is XML, not JSON)' : `(Error parsing JSON: ${e.message})`; }
|
|
if(exampleContentJson) exampleContentJson.textContent = prettyJson;
|
|
if (exampleContentRaw) hljs.highlightElement(exampleContentRaw);
|
|
if (exampleContentJson) hljs.highlightElement(exampleContentJson);
|
|
if(exampleContentWrapper) exampleContentWrapper.style.display = 'block';
|
|
} catch (error) {
|
|
console.error("Error fetching or displaying example:", error);
|
|
if (exampleFilename) exampleFilename.textContent = 'Error loading example';
|
|
if (exampleContentRaw) exampleContentRaw.textContent = `Error: ${error.message}`;
|
|
if (exampleContentJson) exampleContentJson.textContent = `Error: ${error.message}`;
|
|
if (exampleContentWrapper) exampleContentWrapper.style.display = 'block';
|
|
} finally {
|
|
if (exampleLoading) exampleLoading.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// --- Generic Copy Logic Function ---
|
|
function setupCopyButton(buttonElement, sourceElement, tooltipInstance, successTitle, originalTitle, errorTitle, nothingTitle) {
|
|
if (!buttonElement || !sourceElement) { return null; }
|
|
bootstrap.Tooltip.getInstance(buttonElement)?.dispose();
|
|
tooltipInstance = new bootstrap.Tooltip(buttonElement, { title: originalTitle });
|
|
const clickHandler = () => {
|
|
const textToCopy = sourceElement.textContent;
|
|
const copyIcon = buttonElement.querySelector('i');
|
|
const updateTooltip = (newTitle, iconClass) => {
|
|
buttonElement.setAttribute('data-bs-original-title', newTitle);
|
|
tooltipInstance?.setContent({ '.tooltip-inner': newTitle });
|
|
tooltipInstance?.show();
|
|
if (copyIcon) copyIcon.className = `bi ${iconClass}`;
|
|
setTimeout(() => {
|
|
if (buttonElement.isConnected) {
|
|
tooltipInstance?.hide();
|
|
buttonElement.setAttribute('data-bs-original-title', originalTitle);
|
|
if (copyIcon) copyIcon.className = 'bi bi-clipboard';
|
|
}
|
|
}, 1500);
|
|
};
|
|
if (textToCopy && textToCopy.trim() && !textToCopy.startsWith('(Could not') && !textToCopy.startsWith('(Not valid') && !textToCopy.startsWith('Error:')) {
|
|
navigator.clipboard.writeText(textToCopy).then(() => {
|
|
updateTooltip(successTitle, 'bi-check-lg');
|
|
}).catch(err => {
|
|
console.error(`Copy failed:`, err);
|
|
updateTooltip(errorTitle, 'bi-exclamation-triangle');
|
|
});
|
|
} else {
|
|
updateTooltip(nothingTitle, 'bi-clipboard');
|
|
}
|
|
};
|
|
buttonElement.removeEventListener('click', clickHandler);
|
|
buttonElement.addEventListener('click', clickHandler);
|
|
return tooltipInstance;
|
|
}
|
|
|
|
// --- Function to Render Constraints Table ---
|
|
function renderConstraintsTable(elements) {
|
|
const tableBody = document.getElementById('constraints-table')?.querySelector('tbody');
|
|
if (!tableBody) {
|
|
console.error('Constraints table body not found');
|
|
return;
|
|
}
|
|
tableBody.innerHTML = ''; // Clear existing rows
|
|
let constraintsFound = false;
|
|
|
|
if (!elements || !Array.isArray(elements)) {
|
|
console.warn("renderConstraintsTable called with invalid elements data:", elements);
|
|
elements = []; // Prevent errors later
|
|
}
|
|
|
|
elements.forEach(element => {
|
|
if (element && element.constraint && Array.isArray(element.constraint) && element.constraint.length > 0) {
|
|
element.constraint.forEach(constraint => {
|
|
if (constraint) { // Ensure constraint object itself exists
|
|
constraintsFound = true;
|
|
const row = tableBody.insertRow();
|
|
row.insertCell().textContent = constraint.key || 'N/A';
|
|
row.insertCell().textContent = constraint.severity || 'N/A';
|
|
row.insertCell().textContent = element.path || 'N/A'; // Path of the element the constraint is on
|
|
row.insertCell().textContent = constraint.human || 'N/A';
|
|
const expressionCell = row.insertCell();
|
|
const codeElement = document.createElement('code');
|
|
codeElement.className = 'language-fhirpath'; // Use 'language-fhirpath' if you have a FHIRPath grammar for highlight.js
|
|
codeElement.textContent = constraint.expression || 'N/A';
|
|
expressionCell.appendChild(codeElement);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
if (!constraintsFound) {
|
|
const row = tableBody.insertRow();
|
|
const cell = row.insertCell();
|
|
cell.colSpan = 5; // Match number of columns
|
|
cell.textContent = 'No constraints defined in this profile.';
|
|
cell.style.textAlign = 'center';
|
|
}
|
|
|
|
// Re-run highlight.js for the new code blocks
|
|
setTimeout(() => {
|
|
document.querySelectorAll('#constraints-table code.language-fhirpath').forEach((block) => {
|
|
block.classList.remove('hljs');
|
|
hljs.highlightElement(block);
|
|
});
|
|
}, 0);
|
|
}
|
|
|
|
// --- Function to Render Terminology Bindings Table ---
|
|
function renderBindingsTable(elements) {
|
|
const tableBody = document.getElementById('bindings-table')?.querySelector('tbody');
|
|
if (!tableBody) {
|
|
console.error('Bindings table body not found');
|
|
return;
|
|
}
|
|
tableBody.innerHTML = ''; // Clear existing rows
|
|
let bindingsFound = false;
|
|
|
|
if (!elements || !Array.isArray(elements)) {
|
|
console.warn("renderBindingsTable called with invalid elements data:", elements);
|
|
elements = []; // Prevent errors later
|
|
}
|
|
|
|
elements.forEach(element => {
|
|
if (element && element.binding && typeof element.binding === 'object') {
|
|
bindingsFound = true;
|
|
const binding = element.binding;
|
|
const row = tableBody.insertRow();
|
|
row.insertCell().textContent = element.path || 'N/A';
|
|
row.insertCell().textContent = binding.strength || 'N/A';
|
|
const valueSetCell = row.insertCell();
|
|
if (binding.valueSet) {
|
|
const valueSetLink = document.createElement('a');
|
|
valueSetLink.href = binding.valueSet; // Ideally, resolve canonical to actual URL if possible
|
|
const urlParts = binding.valueSet.split('/');
|
|
valueSetLink.textContent = urlParts.length > 1 ? urlParts[urlParts.length - 1] : binding.valueSet;
|
|
valueSetLink.target = '_blank'; // Open in new tab
|
|
valueSetLink.title = `Open ValueSet: ${binding.valueSet}`; // Add tooltip
|
|
valueSetLink.setAttribute('data-bs-toggle', 'tooltip'); // Enable tooltip
|
|
valueSetCell.appendChild(valueSetLink);
|
|
// Re-initialize tooltips for dynamically added links
|
|
const tooltip = new bootstrap.Tooltip(valueSetLink);
|
|
} else {
|
|
valueSetCell.textContent = 'N/A';
|
|
}
|
|
row.insertCell().textContent = binding.description || 'N/A';
|
|
}
|
|
});
|
|
|
|
if (!bindingsFound) {
|
|
const row = tableBody.insertRow();
|
|
const cell = row.insertCell();
|
|
cell.colSpan = 4; // Match number of columns
|
|
cell.textContent = 'No terminology bindings defined in this profile.';
|
|
cell.style.textAlign = 'center';
|
|
}
|
|
}
|
|
|
|
// --- Function to Render Search Parameters Table (Includes Conformance Badges & Tooltips) ---
|
|
function renderSearchParametersTable(searchParamsData) {
|
|
const tableBody = document.getElementById('searchparams-table')?.querySelector('tbody');
|
|
if (!tableBody) {
|
|
console.error('Search Params table body element (#searchparams-table > tbody) not found');
|
|
return;
|
|
}
|
|
|
|
tableBody.innerHTML = '';
|
|
console.log('[renderSearchParametersTable] Received data:', searchParamsData);
|
|
|
|
if (!searchParamsData || !Array.isArray(searchParamsData) || searchParamsData.length === 0) {
|
|
console.log('[renderSearchParametersTable] No valid search parameter data found, displaying message.');
|
|
const row = tableBody.insertRow();
|
|
const cell = row.insertCell();
|
|
cell.colSpan = 4;
|
|
cell.textContent = 'No relevant search parameters found for this resource type.';
|
|
cell.style.textAlign = 'center';
|
|
cell.classList.add('text-muted', 'fst-italic');
|
|
return;
|
|
}
|
|
|
|
searchParamsData.sort((a, b) => (a.code || '').localeCompare(b.code || ''));
|
|
|
|
searchParamsData.forEach((param, index) => {
|
|
console.log(`[renderSearchParametersTable] Processing param ${index + 1}:`, param);
|
|
const row = tableBody.insertRow();
|
|
const nameCell = row.insertCell();
|
|
nameCell.innerHTML = `<code>${escapeHtmlAttr(param.code || 'N/A')}</code>`;
|
|
row.insertCell().textContent = param.type || 'N/A';
|
|
const conformanceCell = row.insertCell();
|
|
const conformanceValue = param.conformance || 'Optional';
|
|
let badgeClass = 'bg-secondary';
|
|
let titleText = 'Conformance level not specified in CapabilityStatement.';
|
|
if (conformanceValue === 'SHALL') {
|
|
badgeClass = 'bg-danger';
|
|
titleText = 'Server SHALL support this parameter.';
|
|
} else if (conformanceValue === 'SHOULD') {
|
|
badgeClass = 'bg-warning text-dark';
|
|
titleText = 'Server SHOULD support this parameter.';
|
|
} else if (conformanceValue === 'MAY') {
|
|
badgeClass = 'bg-info text-dark';
|
|
titleText = 'Server MAY support this parameter.';
|
|
} else if (conformanceValue === 'Optional') {
|
|
badgeClass = 'bg-light text-dark border';
|
|
titleText = 'Server support for this parameter is optional.';
|
|
}
|
|
conformanceCell.setAttribute('title', titleText);
|
|
conformanceCell.setAttribute('data-bs-toggle', 'tooltip');
|
|
conformanceCell.setAttribute('data-bs-placement', 'top');
|
|
conformanceCell.innerHTML = `<span class="badge ${badgeClass}">${escapeHtmlAttr(conformanceValue)}</span>`;
|
|
const existingTooltip = bootstrap.Tooltip.getInstance(conformanceCell);
|
|
if (existingTooltip) { existingTooltip.dispose(); }
|
|
new bootstrap.Tooltip(conformanceCell);
|
|
const descriptionCell = row.insertCell();
|
|
descriptionCell.innerHTML = `<small>${escapeHtmlAttr(param.description || 'No description available.')}</small>`;
|
|
if (param.expression) {
|
|
descriptionCell.setAttribute('title', `FHIRPath: ${param.expression}`);
|
|
descriptionCell.setAttribute('data-bs-toggle', 'tooltip');
|
|
descriptionCell.setAttribute('data-bs-placement', 'top');
|
|
const existingDescTooltip = bootstrap.Tooltip.getInstance(descriptionCell);
|
|
if (existingDescTooltip) { existingDescTooltip.dispose(); }
|
|
new bootstrap.Tooltip(descriptionCell);
|
|
}
|
|
});
|
|
console.log('[renderSearchParametersTable] Finished rendering parameters.');
|
|
} // End renderSearchParametersTable
|
|
|
|
// --- DOMContentLoaded Listener ---
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
try {
|
|
// --- Assign Global Variables to DOM Elements ---
|
|
structureDisplayWrapper = document.getElementById('structure-display-wrapper');
|
|
structureTitle = document.getElementById('structure-title');
|
|
structureLoading = document.getElementById('structure-loading');
|
|
structureFallbackMessage = document.getElementById('structure-fallback-message');
|
|
structureTreeDifferential = document.getElementById('structure-tree-differential');
|
|
structureTreeSnapshot = document.getElementById('structure-tree-snapshot');
|
|
structureTreeMustSupport = document.getElementById('structure-tree-must-support');
|
|
structureTreeKeyElements = document.getElementById('structure-tree-key-elements');
|
|
exampleDisplayWrapper = document.getElementById('example-display-wrapper');
|
|
exampleSelectorWrapper = document.getElementById('example-selector-wrapper');
|
|
exampleSelect = document.getElementById('example-select');
|
|
exampleResourceTypeTitle = document.getElementById('example-resource-type-title');
|
|
exampleLoading = document.getElementById('example-loading');
|
|
exampleContentWrapper = document.getElementById('example-content-wrapper');
|
|
exampleFilename = document.getElementById('example-filename');
|
|
exampleContentRaw = document.getElementById('example-content-raw');
|
|
exampleContentJson = document.getElementById('example-content-json');
|
|
rawStructureWrapper = document.getElementById('raw-structure-wrapper');
|
|
rawStructureTitle = document.getElementById('raw-structure-title');
|
|
rawStructureContent = document.getElementById('raw-structure-content');
|
|
rawStructureLoading = document.getElementById('raw-structure-loading');
|
|
copyRawDefButton = document.getElementById('copy-raw-def-button');
|
|
copyRawExButton = document.getElementById('copy-raw-ex-button');
|
|
copyPrettyJsonButton = document.getElementById('copy-pretty-json-button');
|
|
includeNarrativeCheckbox = document.getElementById('includeNarrative');
|
|
|
|
// --- Initialize Highlight.js ---
|
|
hljs.configure({ languages: ['json'] });
|
|
|
|
// --- Initialize Tooltips for static elements ---
|
|
const staticTooltips = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
|
staticTooltips.forEach(t => { if (!bootstrap.Tooltip.getInstance(t)) { new bootstrap.Tooltip(t); } });
|
|
|
|
// --- Event Listener Setup ---
|
|
copyRawDefTooltipInstance = setupCopyButton(copyRawDefButton, rawStructureContent, copyRawDefTooltipInstance, 'Copied Definition!', 'Copy Raw Definition JSON', 'Copy Failed!', 'Nothing to copy');
|
|
copyRawExTooltipInstance = setupCopyButton(copyRawExButton, exampleContentRaw, copyRawExTooltipInstance, 'Copied Raw!', 'Copy Raw Content', 'Copy Failed!', 'Nothing to copy');
|
|
copyPrettyJsonTooltipInstance = setupCopyButton(copyPrettyJsonButton, exampleContentJson, copyPrettyJsonTooltipInstance, 'Copied JSON!', 'Copy JSON', 'Copy Failed!', 'Nothing to copy');
|
|
|
|
// Resource Type Link Click Handler
|
|
document.querySelectorAll('.resource-type-list').forEach(container => {
|
|
container.addEventListener('click', async function(event) {
|
|
const link = event.target.closest('.resource-type-link');
|
|
if (!link) return;
|
|
event.preventDefault();
|
|
const resourceType = link.dataset.resourceType;
|
|
if (!resourceType) { console.error("Missing data-resource-type"); return; }
|
|
console.log("Resource type link clicked:", resourceType);
|
|
await populateStructure(resourceType);
|
|
populateExampleSelector(resourceType);
|
|
if (structureDisplayWrapper) { structureDisplayWrapper.scrollIntoView({ behavior: 'smooth', block: 'start' }); }
|
|
});
|
|
});
|
|
|
|
// Example Select Dropdown Change Handler
|
|
if (exampleSelect) {
|
|
exampleSelect.addEventListener('change', function(event) { fetchAndDisplayExample(this.value); });
|
|
} else { console.warn("Example select dropdown not found."); }
|
|
|
|
// Narrative Toggle Checkbox Handler
|
|
if (includeNarrativeCheckbox) {
|
|
includeNarrativeCheckbox.addEventListener('change', async function() {
|
|
const selectedFilePath = exampleSelect?.value;
|
|
const resourceType = exampleResourceTypeTitle?.textContent;
|
|
if (selectedFilePath) {
|
|
await fetchAndDisplayExample(selectedFilePath);
|
|
}
|
|
if (resourceType && resourceType !== '(unknown)') {
|
|
await populateStructure(resourceType);
|
|
}
|
|
});
|
|
} else {
|
|
console.warn("Narrative checkbox not found.");
|
|
}
|
|
|
|
// --- Initial UI State ---
|
|
if(structureDisplayWrapper) structureDisplayWrapper.style.display = 'none';
|
|
if(rawStructureWrapper) rawStructureWrapper.style.display = 'none';
|
|
if(exampleDisplayWrapper) exampleDisplayWrapper.style.display = 'none';
|
|
if(structureLoading) structureLoading.style.display = 'none';
|
|
if(rawStructureLoading) rawStructureLoading.style.display = 'none';
|
|
if(exampleLoading) exampleLoading.style.display = 'none';
|
|
if(structureFallbackMessage) structureFallbackMessage.style.display = 'none';
|
|
|
|
} catch (e) {
|
|
console.error("JavaScript error during DOMContentLoaded setup:", e);
|
|
const body = document.querySelector('body');
|
|
if (body) {
|
|
const errorDiv = document.createElement('div');
|
|
errorDiv.className = 'alert alert-danger m-3';
|
|
errorDiv.textContent = 'A critical error occurred while initializing the page view.';
|
|
if(body.firstChild) { body.insertBefore(errorDiv, body.firstChild); } else { body.appendChild(errorDiv); }
|
|
}
|
|
}
|
|
}); // End DOMContentLoaded Listener
|
|
|
|
// --- END OF COMPLETE SCRIPT BLOCK ---
|
|
</script>
|
|
{% endblock %} |