Improve look of plan selection
This commit is contained in:
parent
ad622ef14b
commit
b394eca08b
1 changed files with 395 additions and 113 deletions
|
|
@ -1,34 +1,88 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<style>
|
<style>
|
||||||
.plan-selection .plan-card {
|
.plan-selection .plan-dropdown {
|
||||||
margin-bottom: 0.75rem;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plan-selection .plan-card .card {
|
.plan-selection .plan-dropdown-toggle {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||||
margin-bottom: 0;
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plan-selection .plan-card .card-body {
|
.plan-selection .plan-dropdown-toggle:hover,
|
||||||
|
.plan-selection .plan-dropdown-toggle:focus {
|
||||||
|
border-color: #a1afdf;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-selection .plan-dropdown-toggle[aria-expanded="true"] {
|
||||||
|
border-color: #a1afdf;
|
||||||
|
box-shadow: 0 0 0 0.25rem rgba(67, 94, 190, 0.25);
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
border-bottom-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-selection .plan-dropdown-toggle .dropdown-arrow {
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-selection .plan-dropdown-toggle[aria-expanded="true"] .dropdown-arrow {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-selection .plan-dropdown-menu {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
display: none;
|
||||||
|
margin-top: -1px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #a1afdf;
|
||||||
|
border-top: none;
|
||||||
|
border-radius: 0 0 0.375rem 0.375rem;
|
||||||
|
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-selection .plan-dropdown-menu.show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-selection .plan-option {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-selection .plan-option:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-selection .plan-option:hover,
|
||||||
|
.plan-selection .plan-option.focused {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-selection .plan-option.selected {
|
||||||
|
background-color: #f0f4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-selection .plan-option.selected.focused {
|
||||||
|
background-color: #e0e8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-selection .plan-content {
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plan-selection .plan-card input[type="radio"]:checked+label .card {
|
.plan-selection .plan-header h6 {
|
||||||
border-color: #a1afdf;
|
|
||||||
box-shadow: 0 0 0 0.25rem rgba(67, 94, 190, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
.plan-selection .plan-card .card:hover {
|
|
||||||
border-color: #a1afdf;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plan-selection .form-check-input {
|
|
||||||
position: absolute;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plan-selection h6 {
|
|
||||||
margin-bottom: 0.25rem;
|
margin-bottom: 0.25rem;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
@ -43,17 +97,47 @@
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plan-selection .storage-info {
|
.plan-selection .plan-specs {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-selection .form-check-input {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-selection .storage-card {
|
||||||
margin-top: 0.75rem;
|
margin-top: 0.75rem;
|
||||||
padding: 0.75rem 1rem;
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-selection .storage-card .plan-content {
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
border-left: 3px solid #6c757d;
|
border-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-selection .storage-card .storage-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #e9ecef;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
color: #6c757d;
|
||||||
|
.bi::before {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div class="plan-selection">
|
<div class="plan-selection">
|
||||||
{% if plan_form %}
|
{% if plan_form %}
|
||||||
|
<!-- Hidden radio inputs for form submission -->
|
||||||
|
<div style="display: none;">
|
||||||
{% for assignment in plan_form.fields.compute_plan_assignment.queryset %}
|
{% for assignment in plan_form.fields.compute_plan_assignment.queryset %}
|
||||||
<div class="form-check plan-card">
|
|
||||||
<input class="form-check-input"
|
<input class="form-check-input"
|
||||||
type="radio"
|
type="radio"
|
||||||
name="{{ plan_form.compute_plan_assignment.html_name }}"
|
name="{{ plan_form.compute_plan_assignment.html_name }}"
|
||||||
|
|
@ -64,13 +148,62 @@
|
||||||
data-memory-requests="{{ assignment.compute_plan.memory_requests }}"
|
data-memory-requests="{{ assignment.compute_plan.memory_requests }}"
|
||||||
data-cpu-limits="{{ assignment.compute_plan.cpu_limits }}"
|
data-cpu-limits="{{ assignment.compute_plan.cpu_limits }}"
|
||||||
data-cpu-requests="{{ assignment.compute_plan.cpu_requests }}"
|
data-cpu-requests="{{ assignment.compute_plan.cpu_requests }}"
|
||||||
|
data-plan-name="{{ assignment.compute_plan.name }}"
|
||||||
|
data-plan-sla="{{ assignment.sla }}"
|
||||||
|
data-plan-sla-display="{{ assignment.get_sla_display }}"
|
||||||
|
data-plan-price="{{ assignment.price }}"
|
||||||
|
data-plan-unit="{{ assignment.get_unit_display }}"
|
||||||
required>
|
required>
|
||||||
<label class="form-check-label w-100"
|
{% endfor %}
|
||||||
for="{{ plan_form.compute_plan_assignment.auto_id }}_{{ forloop.counter0 }}">
|
</div>
|
||||||
<div class="card">
|
<div class="plan-dropdown">
|
||||||
<div class="card-body">
|
<div class="plan-dropdown-toggle"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
id="plan-dropdown-toggle">
|
||||||
|
<div class="plan-content">
|
||||||
<div class="d-flex justify-content-between align-items-start">
|
<div class="d-flex justify-content-between align-items-start">
|
||||||
<div>
|
<div class="plan-header">
|
||||||
|
<h6 id="selected-plan-name">-</h6>
|
||||||
|
<span class="badge" id="selected-plan-sla">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-start gap-3">
|
||||||
|
<div class="text-end">
|
||||||
|
<div class="price-display" id="selected-plan-price">-</div>
|
||||||
|
<div class="text-muted small">
|
||||||
|
{% trans "per" %} <span id="selected-plan-unit">-</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown-arrow text-muted">
|
||||||
|
<i class="bi bi-chevron-down"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="plan-specs text-muted small">
|
||||||
|
<i class="bi bi-cpu"></i> <span id="selected-plan-cpu">-</span> {% trans "vCPU" %}
|
||||||
|
<span class="mx-2">•</span>
|
||||||
|
<i class="bi bi-memory"></i> <span id="selected-plan-memory">-</span> {% trans "RAM" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="plan-dropdown-menu" role="listbox" id="plan-dropdown-menu">
|
||||||
|
{% for assignment in plan_form.fields.compute_plan_assignment.queryset %}
|
||||||
|
<div class="plan-option"
|
||||||
|
role="option"
|
||||||
|
data-value="{{ assignment.pk }}"
|
||||||
|
data-plan-name="{{ assignment.compute_plan.name }}"
|
||||||
|
data-plan-sla="{{ assignment.sla }}"
|
||||||
|
data-plan-sla-display="{{ assignment.get_sla_display }}"
|
||||||
|
data-plan-price="{{ assignment.price }}"
|
||||||
|
data-plan-unit="{{ assignment.get_unit_display }}"
|
||||||
|
data-cpu-limits="{{ assignment.compute_plan.cpu_limits }}"
|
||||||
|
data-memory-limits="{{ assignment.compute_plan.memory_limits }}"
|
||||||
|
data-cpu-requests="{{ assignment.compute_plan.cpu_requests }}"
|
||||||
|
data-memory-requests="{{ assignment.compute_plan.memory_requests }}">
|
||||||
|
<div class="d-flex justify-content-between align-items-start">
|
||||||
|
<div class="plan-header">
|
||||||
<h6>{{ assignment.compute_plan.name }}</h6>
|
<h6>{{ assignment.compute_plan.name }}</h6>
|
||||||
<span class="badge bg-{% if assignment.sla == 'guaranteed' %}success{% else %}secondary{% endif %}">
|
<span class="badge bg-{% if assignment.sla == 'guaranteed' %}success{% else %}secondary{% endif %}">
|
||||||
{{ assignment.get_sla_display }}
|
{{ assignment.get_sla_display }}
|
||||||
|
|
@ -81,60 +214,213 @@
|
||||||
<div class="text-muted small">{% trans "per" %} {{ assignment.get_unit_display }}</div>
|
<div class="text-muted small">{% trans "per" %} {{ assignment.get_unit_display }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 text-muted small">
|
<div class="plan-specs text-muted small">
|
||||||
<i class="bi bi-cpu"></i> {{ assignment.compute_plan.cpu_limits }} {% trans "vCPU" %}
|
<i class="bi bi-cpu"></i> {{ assignment.compute_plan.cpu_limits }} {% trans "vCPU" %}
|
||||||
<span class="mx-2">•</span>
|
<span class="mx-2">•</span>
|
||||||
<i class="bi bi-memory"></i> {{ assignment.compute_plan.memory_limits }} {% trans "RAM" %}
|
<i class="bi bi-memory"></i> {{ assignment.compute_plan.memory_limits }} {% trans "RAM" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<div class="storage-info">
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="storage-card">
|
||||||
|
<div class="plan-content">
|
||||||
<div class="d-flex justify-content-between align-items-start">
|
<div class="d-flex justify-content-between align-items-start">
|
||||||
<div>
|
<div class="d-flex align-items-start gap-3">
|
||||||
<strong class="d-block mb-1">{% trans "Storage" %}</strong>
|
<div class="storage-icon">
|
||||||
|
<i class="bi bi-hdd"></i>
|
||||||
|
</div>
|
||||||
|
<div class="plan-header">
|
||||||
|
<h6>{% trans "Storage" %}</h6>
|
||||||
<span class="text-muted small">{% trans "Billed separately based on disk usage" %}</span>
|
<span class="text-muted small">{% trans "Billed separately based on disk usage" %}</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{% if storage_plan %}
|
{% if storage_plan %}
|
||||||
<div class="text-end">
|
<div class="text-end">
|
||||||
<div class="fw-semibold">CHF {{ storage_plan.price_per_gib }}</div>
|
<div class="price-display">CHF {{ storage_plan.price_per_gib }}</div>
|
||||||
<div class="text-muted small">{% trans "per GiB" %}</div>
|
<div class="text-muted small">{% trans "per GiB / hour" %}</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="text-end text-muted small">{% trans "Included" %}</div>
|
<div class="text-end">
|
||||||
|
<span class="badge bg-secondary">{% trans "Included" %}</span>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if storage_plan %}<div class="mt-2 text-muted small" id="storage-cost-display"></div>{% endif %}
|
{% if storage_plan %}<div class="plan-specs text-muted small" id="storage-cost-display"></div>{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="alert alert-warning">{% trans "No compute plans available for this service offering." %}</div>
|
<div class="alert alert-warning">{% trans "No compute plans available for this service offering." %}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
// Update readonly CPU/memory fields when plan selection changes
|
(function() {
|
||||||
document.querySelectorAll('input[name="{{ plan_form.compute_plan_assignment.html_name }}"]').forEach(radio => {
|
const planDropdownToggle = document.getElementById('plan-dropdown-toggle');
|
||||||
radio.addEventListener('change', function() {
|
const planDropdownMenu = document.getElementById('plan-dropdown-menu');
|
||||||
if (this.checked) {
|
const planOptions = Array.from(document.querySelectorAll('.plan-option'));
|
||||||
|
const radioInputs = document.querySelectorAll('input[name="{{ plan_form.compute_plan_assignment.html_name }}"]');
|
||||||
|
let focusedIndex = -1;
|
||||||
|
|
||||||
|
// Update the display in the toggle button based on selected plan
|
||||||
|
function updateSelectedDisplay(data) {
|
||||||
|
document.getElementById('selected-plan-name').textContent = data.planName;
|
||||||
|
document.getElementById('selected-plan-price').textContent = 'CHF ' + data.planPrice;
|
||||||
|
document.getElementById('selected-plan-unit').textContent = data.planUnit;
|
||||||
|
document.getElementById('selected-plan-cpu').textContent = data.cpuLimits;
|
||||||
|
document.getElementById('selected-plan-memory').textContent = data.memoryLimits;
|
||||||
|
|
||||||
|
const slaBadge = document.getElementById('selected-plan-sla');
|
||||||
|
slaBadge.textContent = data.planSlaDisplay;
|
||||||
|
slaBadge.className = 'badge bg-' + (data.planSla === 'guaranteed' ? 'success' : 'secondary');
|
||||||
|
}
|
||||||
|
|
||||||
// Update CPU/memory fields in the form
|
// Update CPU/memory fields in the form
|
||||||
|
function updateFormFields(data) {
|
||||||
const cpuLimit = document.querySelector('input[name="expert-spec.parameters.size.cpu"]');
|
const cpuLimit = document.querySelector('input[name="expert-spec.parameters.size.cpu"]');
|
||||||
const memoryLimit = document.querySelector('input[name="expert-spec.parameters.size.memory"]');
|
const memoryLimit = document.querySelector('input[name="expert-spec.parameters.size.memory"]');
|
||||||
const cpuRequest = document.querySelector('input[name="expert-spec.parameters.size.requests.cpu"]');
|
const cpuRequest = document.querySelector('input[name="expert-spec.parameters.size.requests.cpu"]');
|
||||||
const memoryRequest = document.querySelector('input[name="expert-spec.parameters.size.requests.memory"]');
|
const memoryRequest = document.querySelector('input[name="expert-spec.parameters.size.requests.memory"]');
|
||||||
|
|
||||||
if (cpuLimit) cpuLimit.value = this.dataset.cpuLimits;
|
if (cpuLimit) cpuLimit.value = data.cpuLimits;
|
||||||
if (memoryLimit) memoryLimit.value = this.dataset.memoryLimits;
|
if (memoryLimit) memoryLimit.value = data.memoryLimits;
|
||||||
if (cpuRequest) cpuRequest.value = this.dataset.cpuRequests;
|
if (cpuRequest) cpuRequest.value = data.cpuRequests;
|
||||||
if (memoryRequest) memoryRequest.value = this.dataset.memoryRequests;
|
if (memoryRequest) memoryRequest.value = data.memoryRequests;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Select a plan by value
|
||||||
|
function selectPlan(value) {
|
||||||
|
// Update the hidden radio input
|
||||||
|
radioInputs.forEach(radio => {
|
||||||
|
radio.checked = (radio.value === value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update visual selection
|
||||||
|
planOptions.forEach(option => {
|
||||||
|
option.classList.toggle('selected', option.dataset.value === value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find selected option and update display
|
||||||
|
const selectedOption = document.querySelector('.plan-option[data-value="' + value + '"]');
|
||||||
|
if (selectedOption) {
|
||||||
|
const data = {
|
||||||
|
planName: selectedOption.dataset.planName,
|
||||||
|
planSla: selectedOption.dataset.planSla,
|
||||||
|
planSlaDisplay: selectedOption.dataset.planSlaDisplay,
|
||||||
|
planPrice: selectedOption.dataset.planPrice,
|
||||||
|
planUnit: selectedOption.dataset.planUnit,
|
||||||
|
cpuLimits: selectedOption.dataset.cpuLimits,
|
||||||
|
memoryLimits: selectedOption.dataset.memoryLimits,
|
||||||
|
cpuRequests: selectedOption.dataset.cpuRequests,
|
||||||
|
memoryRequests: selectedOption.dataset.memoryRequests
|
||||||
|
};
|
||||||
|
updateSelectedDisplay(data);
|
||||||
|
updateFormFields(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update focused option visually
|
||||||
|
function updateFocusedOption(newIndex) {
|
||||||
|
planOptions.forEach(opt => opt.classList.remove('focused'));
|
||||||
|
if (newIndex >= 0 && newIndex < planOptions.length) {
|
||||||
|
focusedIndex = newIndex;
|
||||||
|
planOptions[focusedIndex].classList.add('focused');
|
||||||
|
// Scroll into view if needed
|
||||||
|
planOptions[focusedIndex].scrollIntoView({ block: 'nearest' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get index of currently selected option
|
||||||
|
function getSelectedIndex() {
|
||||||
|
return planOptions.findIndex(opt => opt.classList.contains('selected'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open dropdown
|
||||||
|
function openDropdown() {
|
||||||
|
planDropdownToggle.setAttribute('aria-expanded', 'true');
|
||||||
|
planDropdownMenu.classList.add('show');
|
||||||
|
// Set focus to selected option or first option
|
||||||
|
const selectedIdx = getSelectedIndex();
|
||||||
|
updateFocusedOption(selectedIdx >= 0 ? selectedIdx : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close dropdown
|
||||||
|
function closeDropdown() {
|
||||||
|
planDropdownToggle.setAttribute('aria-expanded', 'false');
|
||||||
|
planDropdownMenu.classList.remove('show');
|
||||||
|
planOptions.forEach(opt => opt.classList.remove('focused'));
|
||||||
|
focusedIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if dropdown is open
|
||||||
|
function isOpen() {
|
||||||
|
return planDropdownToggle.getAttribute('aria-expanded') === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle dropdown visibility
|
||||||
|
function toggleDropdown() {
|
||||||
|
if (isOpen()) {
|
||||||
|
closeDropdown();
|
||||||
|
} else {
|
||||||
|
openDropdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
if (planDropdownToggle) {
|
||||||
|
planDropdownToggle.addEventListener('click', toggleDropdown);
|
||||||
|
planDropdownToggle.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (isOpen() && focusedIndex >= 0) {
|
||||||
|
selectPlan(planOptions[focusedIndex].dataset.value);
|
||||||
|
closeDropdown();
|
||||||
|
} else {
|
||||||
|
toggleDropdown();
|
||||||
|
}
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
closeDropdown();
|
||||||
|
} else if (e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!isOpen()) {
|
||||||
|
openDropdown();
|
||||||
|
} else {
|
||||||
|
const newIndex = focusedIndex < planOptions.length - 1 ? focusedIndex + 1 : 0;
|
||||||
|
updateFocusedOption(newIndex);
|
||||||
|
}
|
||||||
|
} else if (e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!isOpen()) {
|
||||||
|
openDropdown();
|
||||||
|
} else {
|
||||||
|
const newIndex = focusedIndex > 0 ? focusedIndex - 1 : planOptions.length - 1;
|
||||||
|
updateFocusedOption(newIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
planOptions.forEach((option, index) => {
|
||||||
|
option.addEventListener('click', function() {
|
||||||
|
selectPlan(this.dataset.value);
|
||||||
|
closeDropdown();
|
||||||
|
planDropdownToggle.focus();
|
||||||
|
});
|
||||||
|
option.addEventListener('mouseenter', function() {
|
||||||
|
updateFocusedOption(index);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Trigger initial update
|
// Close dropdown when clicking outside
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
if (planDropdownToggle && !planDropdownToggle.contains(e.target) && !planDropdownMenu.contains(e.target)) {
|
||||||
|
closeDropdown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize with currently checked radio
|
||||||
const checkedRadio = document.querySelector('input[name="{{ plan_form.compute_plan_assignment.html_name }}"]:checked');
|
const checkedRadio = document.querySelector('input[name="{{ plan_form.compute_plan_assignment.html_name }}"]:checked');
|
||||||
if (checkedRadio) {
|
if (checkedRadio) {
|
||||||
checkedRadio.dispatchEvent(new Event('change'));
|
selectPlan(checkedRadio.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup storage cost calculator
|
// Setup storage cost calculator
|
||||||
|
|
@ -144,12 +430,7 @@
|
||||||
diskInput.dataset.storageListenerAttached = 'true';
|
diskInput.dataset.storageListenerAttached = 'true';
|
||||||
diskInput.addEventListener('input', function() {
|
diskInput.addEventListener('input', function() {
|
||||||
const sizeGiB = parseFloat(this.value) || 0;
|
const sizeGiB = parseFloat(this.value) || 0;
|
||||||
const pricePerGiB = {
|
const pricePerGiB = {{ storage_plan.price_per_gib|default:0 }};
|
||||||
{
|
|
||||||
storage_plan.price_per_gib |
|
|
||||||
default: 0
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const totalCost = (sizeGiB * pricePerGiB).toFixed(2);
|
const totalCost = (sizeGiB * pricePerGiB).toFixed(2);
|
||||||
const display = document.getElementById('storage-cost-display');
|
const display = document.getElementById('storage-cost-display');
|
||||||
if (display && sizeGiB > 0) {
|
if (display && sizeGiB > 0) {
|
||||||
|
|
@ -175,4 +456,5 @@
|
||||||
setupStorageCostCalculator();
|
setupStorageCostCalculator();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue