mirror of
https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit.git
synced 2025-09-17 18:35:02 +00:00
V4.3 Advanced Validation added full gamut of options and configuration for fully fleshed out validation
588 lines
28 KiB
HTML
588 lines
28 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">Validate FHIR Sample</h1>
|
|
<div class="col-lg-6 mx-auto">
|
|
<p class="lead mb-4">
|
|
Validate a FHIR resource or bundle against a selected Implementation Guide.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="spinner-overlay" class="d-none position-fixed top-0 start-0 w-100 h-100 bg-dark bg-opacity-50 d-flex align-items-center justify-content-center" style="z-index: 1050;">
|
|
<div class="text-center text-white">
|
|
<div id="spinner-animation" style="width: 200px; height: 200px;"></div>
|
|
<p class="mt-3" id="spinner-message">Waiting, don't leave this page...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="container mt-4">
|
|
<div class="card">
|
|
<div class="card-header">Validation Form</div>
|
|
<div class="card-body">
|
|
<form id="validationForm" method="POST" action="/api/validate">
|
|
{{ form.csrf_token }}
|
|
|
|
<div class="mb-3">
|
|
<small class="form-text text-muted">
|
|
Select a package from the list below or enter a new one (e.g., <code>hl7.fhir.us.core</code>).
|
|
</small>
|
|
<div class="mt-2">
|
|
<select class="form-select" id="packageSelect">
|
|
<option value="">-- Select a Package --</option>
|
|
{% if packages %}
|
|
{% for pkg in packages %}
|
|
<option value="{{ pkg.name }}#{{ pkg.version }}">{{ pkg.name }}#{{ pkg.version }}</option>
|
|
{% endfor %}
|
|
{% else %}
|
|
<option value="" disabled>No packages available</option>
|
|
{% endif %}
|
|
</select>
|
|
</div>
|
|
<label for="{{ form.package_name.id }}" class="form-label">Package Name</label>
|
|
{{ form.package_name(class="form-control", id=form.package_name.id) }}
|
|
{% for error in form.package_name.errors %}
|
|
<div class="text-danger">{{ error }}</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="{{ form.version.id }}" class="form-label">Package Version</label>
|
|
{{ form.version(class="form-control", id=form.version.id) }}
|
|
{% for error in form.version.errors %}
|
|
<div class="text-danger">{{ error }}</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<hr class="my-4">
|
|
|
|
<!-- New Fields from ValidationForm -->
|
|
<div class="mb-3">
|
|
<label for="fhir_resource" class="form-label">FHIR Resource (JSON or XML)</label>
|
|
{{ form.fhir_resource(class="form-control", rows=10, placeholder="Paste your FHIR JSON here...") }}
|
|
</div>
|
|
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label for="fhir_version" class="form-label">FHIR Version</label>
|
|
{{ form.fhir_version(class="form-select") }}
|
|
</div>
|
|
</div>
|
|
|
|
<hr class="my-4">
|
|
|
|
<h5>Validation Flags</h5>
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<div class="form-check">
|
|
{{ form.do_native(class="form-check-input") }}
|
|
<label class="form-check-label" for="do_native">{{ form.do_native.label }}</label>
|
|
</div>
|
|
<div class="form-check">
|
|
{{ form.hint_about_must_support(class="form-check-input") }}
|
|
<label class="form-check-label" for="hint_about_must_support">{{ form.hint_about_must_support.label }}</label>
|
|
</div>
|
|
<div class="form-check">
|
|
{{ form.assume_valid_rest_references(class="form-check-input") }}
|
|
<label class="form-check-label" for="assume_valid_rest_references">{{ form.assume_valid_rest_references.label }}</label>
|
|
</div>
|
|
<div class="form-check">
|
|
{{ form.no_extensible_binding_warnings(class="form-check-input") }}
|
|
<label class="form-check-label" for="no_extensible_binding_warnings">{{ form.no_extensible_binding_warnings.label }}</label>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="form-check">
|
|
{{ form.show_times(class="form-check-input") }}
|
|
<label class="form-check-label" for="show_times">{{ form.show_times.label }}</label>
|
|
</div>
|
|
<div class="form-check">
|
|
{{ form.allow_example_urls(class="form-check-input") }}
|
|
<label class="form-check-label" for="allow_example_urls">{{ form.allow_example_urls.label }}</label>
|
|
</div>
|
|
<div class="form-check">
|
|
{{ form.check_ips_codes(class="form-check-input") }}
|
|
<label class="form-check-label" for="check_ips_codes">{{ form.check_ips_codes.label }}</label>
|
|
</div>
|
|
<div class="form-check">
|
|
{{ form.allow_any_extensions(class="form-check-input") }}
|
|
<label class="form-check-label" for="allow_any_extensions">{{ form.allow_any_extensions.label }}</label>
|
|
</div>
|
|
<div class="form-check">
|
|
{{ form.tx_routing(class="form-check-input") }}
|
|
<label class="form-check-label" for="tx_routing">{{ form.tx_routing.label }}</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<hr class="my-4">
|
|
|
|
<h5>Other Settings</h5>
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label for="profiles" class="form-label">{{ form.profiles.label }}</label>
|
|
{{ form.profiles(class="form-control") }}
|
|
<small class="form-text text-muted">Comma-separated URLs, e.g., url1,url2</small>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label for="extensions" class="form-label">{{ form.extensions.label }}</label>
|
|
{{ form.extensions(class="form-control") }}
|
|
<small class="form-text text-muted">Comma-separated URLs, e.g., url1,url2</small>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label for="terminology_server" class="form-label">{{ form.terminology_server.label }}</label>
|
|
{{ form.terminology_server(class="form-control") }}
|
|
<small class="form-text text-muted">e.g., http://tx.fhir.org</small>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label for="snomed_ct_version" class="form-label">{{ form.snomed_ct_version.label }}</label>
|
|
{{ form.snomed_ct_version(class="form-select") }}
|
|
<small class="form-text text-muted">Select a SNOMED CT edition.</small>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-4 text-center">
|
|
<button type="submit" id="validateButton" class="btn btn-primary btn-lg">
|
|
Validate Resource
|
|
</button>
|
|
<button type="button" id="clearResultsButton" class="btn btn-secondary btn-lg d-none">
|
|
Clear Results
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="validationResult" class="card mt-4" style="display: none;">
|
|
<div class="card-header">Validation Results</div>
|
|
<div class="card-body" id="validationContent">
|
|
<!-- Results will be displayed here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="{{ url_for('static', filename='js/lottie.min.js') }}"></script>
|
|
<script>
|
|
// --- Global Variables and Helper Functions ---
|
|
let currentLottieAnimation = null;
|
|
let pollInterval = null;
|
|
const spinnerOverlay = document.getElementById('spinner-overlay');
|
|
const spinnerMessage = document.getElementById('spinner-message');
|
|
const spinnerContainer = document.getElementById('spinner-animation');
|
|
const perPage = 10;
|
|
|
|
// Function to load the Lottie animation based on theme
|
|
function loadLottieAnimation(theme) {
|
|
if (currentLottieAnimation) {
|
|
currentLottieAnimation.destroy();
|
|
currentLottieAnimation = null;
|
|
}
|
|
const animationPath = (theme === 'dark')
|
|
? '{{ url_for('static', filename='animations/Validation abstract.json') }}'
|
|
: '{{ url_for('static', filename='animations/Validation abstract.json') }}';
|
|
|
|
try {
|
|
currentLottieAnimation = lottie.loadAnimation({
|
|
container: spinnerContainer,
|
|
renderer: 'svg',
|
|
loop: true,
|
|
autoplay: true,
|
|
path: animationPath
|
|
});
|
|
} catch(lottieError) {
|
|
console.error("Error loading Lottie animation:", lottieError);
|
|
}
|
|
}
|
|
|
|
// Function to show/hide the spinner with a custom message
|
|
function toggleSpinner(show, message = "Running validator...") {
|
|
if (show) {
|
|
spinnerMessage.textContent = message;
|
|
spinnerOverlay.classList.remove('d-none');
|
|
const currentTheme = document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light';
|
|
loadLottieAnimation(currentTheme);
|
|
} else {
|
|
spinnerOverlay.classList.add('d-none');
|
|
if (currentLottieAnimation) {
|
|
currentLottieAnimation.destroy();
|
|
currentLottieAnimation = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Frontend logic starts here
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const form = document.getElementById('validationForm');
|
|
const validateButton = document.getElementById('validateButton');
|
|
const clearResultsButton = document.getElementById('clearResultsButton');
|
|
const validationResult = document.getElementById('validationResult');
|
|
const validationContent = document.getElementById('validationContent');
|
|
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
|
|
const packageSelect = document.getElementById('packageSelect');
|
|
const packageNameInput = document.getElementById('{{ form.package_name.id }}');
|
|
const versionInput = document.getElementById('{{ form.version.id }}');
|
|
const fhirResourceInput = document.getElementById('fhir_resource');
|
|
|
|
// Function to update package fields when a package is selected from the dropdown
|
|
function updatePackageFields(select) {
|
|
const value = select.value;
|
|
if (!packageNameInput || !versionInput) return;
|
|
if (value) {
|
|
const [name, version] = value.split('#');
|
|
if (name && version) {
|
|
packageNameInput.value = name;
|
|
versionInput.value = version;
|
|
} else {
|
|
packageNameInput.value = '';
|
|
versionInput.value = '';
|
|
}
|
|
} else {
|
|
packageNameInput.value = '';
|
|
versionInput.value = '';
|
|
}
|
|
}
|
|
|
|
// Sanitize text to prevent XSS
|
|
function sanitizeText(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// Tidy up the display by paginating results
|
|
function renderResultList(container, items, title, isWarnings = false) {
|
|
container.innerHTML = '';
|
|
if (items.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const wrapper = document.createElement('div');
|
|
const listId = `list-${title.toLowerCase()}`;
|
|
|
|
let initialItems = items.slice(0, perPage);
|
|
let remainingItems = items.slice(perPage);
|
|
|
|
const titleElement = document.createElement('h5');
|
|
titleElement.textContent = title;
|
|
wrapper.appendChild(titleElement);
|
|
|
|
const ul = document.createElement('ul');
|
|
ul.className = 'list-unstyled';
|
|
initialItems.forEach(item => {
|
|
const li = document.createElement('li');
|
|
li.innerHTML = `
|
|
<div class="alert alert-${isWarnings ? 'warning' : 'danger'}">
|
|
${sanitizeText(typeof item === 'string' ? item : item.message || item.issue)}
|
|
${isWarnings && (item.issue && item.issue.includes('Must Support')) ? ' <i class="bi bi-info-circle" title="This element is marked as Must Support in the profile."></i>' : ''}
|
|
</div>
|
|
`;
|
|
ul.appendChild(li);
|
|
});
|
|
wrapper.appendChild(ul);
|
|
|
|
if (remainingItems.length > 0) {
|
|
const toggleButton = document.createElement('a');
|
|
toggleButton.href = '#';
|
|
toggleButton.className = 'btn btn-sm btn-link text-decoration-none';
|
|
toggleButton.textContent = `Show More (${remainingItems.length} more)`;
|
|
|
|
const remainingUl = document.createElement('ul');
|
|
remainingUl.className = 'list-unstyled d-none';
|
|
remainingItems.forEach(item => {
|
|
const li = document.createElement('li');
|
|
li.innerHTML = `
|
|
<div class="alert alert-${isWarnings ? 'warning' : 'danger'}">
|
|
${sanitizeText(typeof item === 'string' ? item : item.message || item.issue)}
|
|
${isWarnings && (item.issue && item.issue.includes('Must Support')) ? ' <i class="bi bi-info-circle" title="This element is marked as Must Support in the profile."></i>' : ''}
|
|
</div>
|
|
`;
|
|
remainingUl.appendChild(li);
|
|
});
|
|
|
|
toggleButton.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
if (remainingUl.classList.contains('d-none')) {
|
|
remainingUl.classList.remove('d-none');
|
|
toggleButton.textContent = 'Show Less';
|
|
} else {
|
|
remainingUl.classList.add('d-none');
|
|
toggleButton.textContent = `Show More (${remainingItems.length} more)`;
|
|
}
|
|
});
|
|
wrapper.appendChild(toggleButton);
|
|
wrapper.appendChild(remainingUl);
|
|
}
|
|
container.appendChild(wrapper);
|
|
}
|
|
|
|
function displayFinalReport(data) {
|
|
validationContent.innerHTML = '';
|
|
let hasContent = false;
|
|
|
|
clearResultsButton.classList.remove('d-none');
|
|
|
|
if (data.file_paths) {
|
|
// Use the paths returned from the server to create download links
|
|
const jsonFileName = data.file_paths.json.split('/').pop();
|
|
const htmlFileName = data.file_paths.html.split('/').pop();
|
|
const downloadDiv = document.createElement('div');
|
|
downloadDiv.className = 'alert alert-info d-flex justify-content-between align-items-center';
|
|
downloadDiv.innerHTML = `
|
|
<span>Download Raw Reports:</span>
|
|
<div>
|
|
<a href="/download/${jsonFileName}" class="btn btn-sm btn-secondary me-2" download>Download JSON</a>
|
|
<a href="/download/${htmlFileName}" class="btn btn-sm btn-secondary" download>Download HTML</a>
|
|
</div>
|
|
`;
|
|
validationContent.appendChild(downloadDiv);
|
|
hasContent = true;
|
|
}
|
|
|
|
if (data.errors && data.errors.length > 0) {
|
|
hasContent = true;
|
|
const errorContainer = document.createElement('div');
|
|
renderResultList(errorContainer, data.errors, 'Errors');
|
|
validationContent.appendChild(errorContainer);
|
|
}
|
|
|
|
if (data.warnings && data.warnings.length > 0) {
|
|
hasContent = true;
|
|
const warningContainer = document.createElement('div');
|
|
renderResultList(warningContainer, data.warnings, 'Warnings', true);
|
|
validationContent.appendChild(warningContainer);
|
|
}
|
|
|
|
if (data.results) {
|
|
hasContent = true;
|
|
const resultsDiv = document.createElement('div');
|
|
resultsDiv.innerHTML = '<h5>Detailed Results</h5>';
|
|
let index = 0;
|
|
for (const [resourceId, result] of Object.entries(data.results)) {
|
|
const collapseId = `result-collapse-${index++}`;
|
|
resultsDiv.innerHTML += `
|
|
<div class="card mb-2">
|
|
<div class="card-header" role="button" data-bs-toggle="collapse" data-bs-target="#${collapseId}" aria-expanded="false" aria-controls="${collapseId}">
|
|
<h6 class="mb-0">${sanitizeText(resourceId)}</h6>
|
|
<p class="mb-0"><strong>Valid:</strong> ${result.valid ? 'Yes' : 'No'}</p>
|
|
</div>
|
|
<div id="${collapseId}" class="collapse">
|
|
<div class="card-body">
|
|
${result.details && result.details.length > 0 ? `
|
|
<ul>
|
|
${result.details.map(d => `
|
|
<li class="${d.severity === 'error' ? 'text-danger' : d.severity === 'warning' ? 'text-warning' : 'text-info'}">
|
|
${sanitizeText(d.issue)}
|
|
${d.description && d.description !== d.issue ? `<br><small class="text-muted">${sanitizeText(d.description)}</small>` : ''}
|
|
</li>
|
|
`).join('')}
|
|
</ul>
|
|
` : '<p>No additional details.</p>'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
validationContent.appendChild(resultsDiv);
|
|
}
|
|
|
|
if (data.summary) {
|
|
const summaryDiv = document.createElement('div');
|
|
summaryDiv.className = 'alert alert-info';
|
|
summaryDiv.innerHTML = `
|
|
<h5>Summary</h5>
|
|
<p>Valid: ${data.valid ? 'Yes' : 'No'}</p>
|
|
<p>Errors: ${data.summary.error_count || 0}, Warnings: ${data.summary.warning_count || 0}</p>
|
|
`;
|
|
validationContent.appendChild(summaryDiv);
|
|
hasContent = true;
|
|
}
|
|
|
|
if (!hasContent && data.valid) {
|
|
validationContent.innerHTML = '<div class="alert alert-success">Validation passed with no errors or warnings.</div>';
|
|
}
|
|
|
|
document.querySelectorAll('[data-bs-toggle="collapse"]').forEach(el => {
|
|
el.addEventListener('click', () => {
|
|
const target = document.querySelector(el.getAttribute('data-bs-target'));
|
|
target.classList.toggle('show');
|
|
});
|
|
});
|
|
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => {
|
|
new bootstrap.Tooltip(el);
|
|
});
|
|
}
|
|
|
|
function checkValidationStatus(taskId) {
|
|
fetch(`/api/check-validation-status/${taskId}`)
|
|
.then(response => {
|
|
if (response.status === 202) {
|
|
console.log('Validation still in progress...');
|
|
return null;
|
|
} else if (response.status === 200) {
|
|
clearInterval(pollInterval);
|
|
toggleSpinner(false);
|
|
return response.json();
|
|
} else {
|
|
clearInterval(pollInterval);
|
|
toggleSpinner(false);
|
|
return response.json().then(errorData => {
|
|
throw new Error(errorData.message);
|
|
});
|
|
}
|
|
})
|
|
.then(data => {
|
|
if (data && data.status === 'complete') {
|
|
validationResult.style.display = 'block';
|
|
displayFinalReport(data.result);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
clearInterval(pollInterval);
|
|
toggleSpinner(false);
|
|
console.error('Validation failed during polling:', error);
|
|
validationResult.style.display = 'block';
|
|
validationContent.innerHTML = `<div class="alert alert-danger">An unexpected error occurred during validation: ${sanitizeText(error.message)}</div>`;
|
|
});
|
|
}
|
|
|
|
function runBlockingValidation(e) {
|
|
e.preventDefault(); // This is the crucial line to prevent default form submission
|
|
const packageName = document.getElementById('{{ form.package_name.id }}').value;
|
|
const version = document.getElementById('{{ form.version.id }}').value;
|
|
const sampleData = document.getElementById('fhir_resource').value.trim();
|
|
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
|
|
|
|
if (!packageName || !version) {
|
|
validationResult.style.display = 'block';
|
|
validationContent.innerHTML = '<div class="alert alert-danger">Please select a package and version.</div>';
|
|
return;
|
|
}
|
|
|
|
if (!sampleData) {
|
|
validationResult.style.display = 'block';
|
|
validationContent.innerHTML = '<div class="alert alert-danger">Please provide FHIR sample JSON or XML.</div>';
|
|
return;
|
|
}
|
|
|
|
toggleSpinner(true, "Running FHIR Validator (This may take up to 5 minutes on the first run).");
|
|
|
|
// Collect all form data including the new fields
|
|
const data = {
|
|
package_name: packageName,
|
|
version: version,
|
|
fhir_resource: sampleData,
|
|
fhir_version: document.getElementById('fhir_version').value,
|
|
do_native: document.getElementById('do_native').checked,
|
|
hint_about_must_support: document.getElementById('hint_about_must_support').checked,
|
|
assume_valid_rest_references: document.getElementById('assume_valid_rest_references').checked,
|
|
no_extensible_binding_warnings: document.getElementById('no_extensible_binding_warnings').checked,
|
|
show_times: document.getElementById('show_times').checked,
|
|
allow_example_urls: document.getElementById('allow_example_urls').checked,
|
|
check_ips_codes: document.getElementById('check_ips_codes').checked,
|
|
allow_any_extensions: document.getElementById('allow_any_extensions').checked,
|
|
tx_routing: document.getElementById('tx_routing').checked,
|
|
snomed_ct_version: document.getElementById('snomed_ct_version').value,
|
|
profiles: document.getElementById('profiles').value,
|
|
extensions: document.getElementById('extensions').value,
|
|
terminology_server: document.getElementById('terminology_server').value
|
|
};
|
|
|
|
fetch('/api/validate', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': csrfToken
|
|
},
|
|
body: JSON.stringify(data)
|
|
})
|
|
.then(response => {
|
|
if (response.status === 202) {
|
|
return response.json();
|
|
} else {
|
|
toggleSpinner(false);
|
|
return response.json().then(errorData => {
|
|
throw new Error(errorData.message);
|
|
});
|
|
}
|
|
})
|
|
.then(data => {
|
|
if (data.status === 'accepted' && data.task_id) {
|
|
pollInterval = setInterval(() => checkValidationStatus(data.task_id), 3000);
|
|
} else {
|
|
toggleSpinner(false);
|
|
validationResult.style.display = 'block';
|
|
validationContent.innerHTML = `<div class="alert alert-danger">Server response format unexpected.</div>`;
|
|
}
|
|
})
|
|
.catch(error => {
|
|
clearInterval(pollInterval);
|
|
toggleSpinner(false);
|
|
console.error('Validation failed:', error);
|
|
validationResult.style.display = 'block';
|
|
validationContent.innerHTML = `<div class="alert alert-danger">An unexpected error occurred during validation: ${sanitizeText(error.message)}</div>`;
|
|
});
|
|
};
|
|
|
|
clearResultsButton.addEventListener('click', function() {
|
|
fetch('/api/clear-validation-results', {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': csrfToken
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
validationResult.style.display = 'none';
|
|
clearResultsButton.classList.add('d-none');
|
|
} else {
|
|
console.error('Failed to clear results:', data.message);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Network error while clearing results:', error);
|
|
});
|
|
});
|
|
|
|
if (packageSelect) {
|
|
packageSelect.addEventListener('change', function() {
|
|
updatePackageFields(this);
|
|
});
|
|
}
|
|
|
|
if (validateButton) {
|
|
validateButton.addEventListener('click', function(e) {
|
|
runBlockingValidation(e);
|
|
});
|
|
}
|
|
|
|
if (packageNameInput.value && versionInput.value) {
|
|
const currentValue = `${packageNameInput.value}#${versionInput.value}`;
|
|
const optionExists = Array.from(packageSelect.options).some(opt => opt.value === currentValue);
|
|
if (optionExists) {
|
|
packageSelect.value = currentValue;
|
|
}
|
|
}
|
|
|
|
if (fhirResourceInput) {
|
|
fhirResourceInput.addEventListener('input', function() {
|
|
try {
|
|
if (this.value.trim()) {
|
|
JSON.parse(this.value);
|
|
this.classList.remove('is-invalid');
|
|
this.classList.add('is-valid');
|
|
} else {
|
|
this.classList.remove('is-valid', 'is-invalid');
|
|
}
|
|
} catch (e) {
|
|
this.classList.remove('is-valid');
|
|
this.classList.add('is-invalid');
|
|
}
|
|
});
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %}
|