Enhance navigation after save by improving URL handling and fallback mechanisms for question transitions
This commit is contained in:
@@ -36,7 +36,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<p><span id="question-stem">{{ question.stem|safe }}</span></p>
|
<p><span id="question-stem">{{ question.stem|safe }}</span></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div id="question-fragment"
|
<div id="question-fragment"
|
||||||
hx-trigger="load"
|
hx-trigger="load"
|
||||||
@@ -85,20 +85,20 @@
|
|||||||
.exam-take-container { max-width: 980px; margin: 0 auto; padding: 1rem; }
|
.exam-take-container { max-width: 980px; margin: 0 auto; padding: 1rem; }
|
||||||
.no-select { user-select: none; }
|
.no-select { user-select: none; }
|
||||||
|
|
||||||
.flex-container { display: flex; align-items: center; gap: 0.75rem; }
|
.flex-container { display: flex; align-items: center; gap: 0.75rem; }
|
||||||
/* fieldWrapper is used around each label + input; ensure it stretches the full width */
|
/* fieldWrapper is used around each label + input; ensure it stretches the full width */
|
||||||
.fieldWrapper { width: 100%; display: flex; align-items: center; gap: 0.75rem; }
|
.fieldWrapper { width: 100%; display: flex; align-items: center; gap: 0.75rem; }
|
||||||
|
|
||||||
/* We keep two main columns inside each fieldWrapper:
|
/* We keep two main columns inside each fieldWrapper:
|
||||||
- the label (question-text) which should take the bulk of space and sit to the left on wide screens
|
- the label (question-text) which should take the bulk of space and sit to the left on wide screens
|
||||||
- the input container (.flex-1) which holds the input and a post-input label; the post-input should float right
|
- the input container (.flex-1) which holds the input and a post-input label; the post-input should float right
|
||||||
*/
|
*/
|
||||||
.flex-8 { flex: 8; }
|
.flex-8 { flex: 8; }
|
||||||
.flex-1 { flex: 1; display: flex; justify-content: space-between; align-items: center; gap: 0.5rem; }
|
.flex-1 { flex: 1; display: flex; justify-content: space-between; align-items: center; gap: 0.5rem; }
|
||||||
|
|
||||||
/* Left-align label text on wide screens so the question text appears on the left, input between, and the
|
/* Left-align label text on wide screens so the question text appears on the left, input between, and the
|
||||||
post-input label floats to the right. Padding keeps visual separation. */
|
post-input label floats to the right. Padding keeps visual separation. */
|
||||||
.question-text { margin: 0; text-align: left; padding-right: 0.5rem; }
|
.question-text { margin: 0; text-align: left; padding-right: 0.5rem; }
|
||||||
|
|
||||||
/* Answer items styled as cards */
|
/* Answer items styled as cards */
|
||||||
.physics-answer-list { list-style: none; padding: 0; margin: 0; }
|
.physics-answer-list { list-style: none; padding: 0; margin: 0; }
|
||||||
@@ -118,8 +118,8 @@
|
|||||||
.question-text { margin: 0; }
|
.question-text { margin: 0; }
|
||||||
|
|
||||||
/* Post-input badge (shows True/False text next to inputs) */
|
/* Post-input badge (shows True/False text next to inputs) */
|
||||||
.postinput { display: inline-block; min-width: 3rem; text-align: right; color: var(--muted); }
|
.postinput { display: inline-block; min-width: 3rem; text-align: right; color: var(--muted); }
|
||||||
.physics-answer-list input { margin: 0 0.25rem 0 0; }
|
.physics-answer-list input { margin: 0 0.25rem 0 0; }
|
||||||
/* show a compact post-label on the right of the input container */
|
/* show a compact post-label on the right of the input container */
|
||||||
.physics-answer-list input:checked + .postinput::after {
|
.physics-answer-list input:checked + .postinput::after {
|
||||||
content: 'True'; color: var(--success); font-weight: 600; margin-left: 0.4rem;
|
content: 'True'; color: var(--success); font-weight: 600; margin-left: 0.4rem;
|
||||||
@@ -177,15 +177,15 @@
|
|||||||
.mt-2 .save.btn.btn-default { background: var(--feedback-bg); border: 1px solid var(--card-border); color: var(--text); }
|
.mt-2 .save.btn.btn-default { background: var(--feedback-bg); border: 1px solid var(--card-border); color: var(--text); }
|
||||||
|
|
||||||
/* Overview button - make it stand out using the primary color */
|
/* Overview button - make it stand out using the primary color */
|
||||||
#overview-button { background: var(--primary); border-color: var(--primary); color: #fff; }
|
#overview-button { background: var(--primary); border-color: var(--primary); color: #fff; }
|
||||||
#overview-button:hover { filter: brightness(0.95); box-shadow: 0 3px 8px rgba(13,110,253,0.18); }
|
#overview-button:hover { filter: brightness(0.95); box-shadow: 0 3px 8px rgba(13,110,253,0.18); }
|
||||||
|
|
||||||
/* Previous / Next button colors */
|
/* Previous / Next button colors */
|
||||||
.mt-2 button[name="previous"] { background: var(--info); border-color: var(--info); color: #fff; }
|
.mt-2 button[name="previous"] { background: var(--info); border-color: var(--info); color: #fff; }
|
||||||
.mt-2 button[name="previous"]:hover { filter: brightness(0.95); box-shadow: 0 2px 6px rgba(0,0,0,0.08); }
|
.mt-2 button[name="previous"]:hover { filter: brightness(0.95); box-shadow: 0 2px 6px rgba(0,0,0,0.08); }
|
||||||
|
|
||||||
.mt-2 button[name="next"] { background: var(--info); border-color: var(--info); color: #fff; }
|
.mt-2 button[name="next"] { background: var(--info); border-color: var(--info); color: #fff; }
|
||||||
.mt-2 button[name="next"]:hover { filter: brightness(0.95); box-shadow: 0 3px 8px rgba(13,110,253,0.18); }
|
.mt-2 button[name="next"]:hover { filter: brightness(0.95); box-shadow: 0 3px 8px rgba(13,110,253,0.18); }
|
||||||
|
|
||||||
/* Small helper: make labels wrap nicely on small screens */
|
/* Small helper: make labels wrap nicely on small screens */
|
||||||
@media (max-width: 576px) {
|
@media (max-width: 576px) {
|
||||||
@@ -201,77 +201,77 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
<script>
|
<script>
|
||||||
/* HTMX swap helpers: preserve scroll and container height to avoid page jumping
|
/* HTMX swap helpers: preserve scroll and container height to avoid page jumping
|
||||||
Listens for htmx:beforeSwap and htmx:afterSwap on the question fragment container.
|
Listens for htmx:beforeSwap and htmx:afterSwap on the question fragment container.
|
||||||
*/
|
*/
|
||||||
(function(){
|
(function(){
|
||||||
function getTarget(evt){
|
function getTarget(evt){
|
||||||
return (evt.detail && (evt.detail.target || evt.detail.elt)) || evt.target;
|
return (evt.detail && (evt.detail.target || evt.detail.elt)) || evt.target;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('htmx:beforeSwap', function(evt){
|
document.addEventListener('htmx:beforeSwap', function(evt){
|
||||||
try{
|
try{
|
||||||
var target = getTarget(evt);
|
var target = getTarget(evt);
|
||||||
if(!target || target.id !== 'question-fragment') return;
|
if(!target || target.id !== 'question-fragment') return;
|
||||||
// save scroll position
|
// save scroll position
|
||||||
target.__savedScrollY = window.scrollY || window.pageYOffset || 0;
|
target.__savedScrollY = window.scrollY || window.pageYOffset || 0;
|
||||||
// preserve current height to avoid layout shift while fragment is swapping
|
// preserve current height to avoid layout shift while fragment is swapping
|
||||||
target.style.minHeight = target.offsetHeight + 'px';
|
target.style.minHeight = target.offsetHeight + 'px';
|
||||||
}catch(e){ console && console.error && console.error(e); }
|
}catch(e){ console && console.error && console.error(e); }
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('htmx:afterSwap', function(evt){
|
document.addEventListener('htmx:afterSwap', function(evt){
|
||||||
try{
|
try{
|
||||||
var target = getTarget(evt);
|
var target = getTarget(evt);
|
||||||
if(!target || target.id !== 'question-fragment') return;
|
if(!target || target.id !== 'question-fragment') return;
|
||||||
var y = target.__savedScrollY;
|
var y = target.__savedScrollY;
|
||||||
if(typeof y === 'number') window.scrollTo(0, y);
|
if(typeof y === 'number') window.scrollTo(0, y);
|
||||||
// remove the minHeight after a short delay to allow content to layout
|
// remove the minHeight after a short delay to allow content to layout
|
||||||
setTimeout(function(){ target.style.minHeight = ''; }, 60);
|
setTimeout(function(){ target.style.minHeight = ''; }, 60);
|
||||||
// notify fragment-loaded so fragment init code can listen if required
|
// notify fragment-loaded so fragment init code can listen if required
|
||||||
target.dispatchEvent(new CustomEvent('fragment:loaded', { bubbles: true }));
|
target.dispatchEvent(new CustomEvent('fragment:loaded', { bubbles: true }));
|
||||||
}catch(e){ console && console.error && console.error(e); }
|
}catch(e){ console && console.error && console.error(e); }
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
// Global listener for server-triggered save confirmations.
|
// Global listener for server-triggered save confirmations.
|
||||||
// The view sets an `HX-Trigger` header named `saved` when an HTMX save occurs.
|
// The view sets an `HX-Trigger` header named `saved` when an HTMX save occurs.
|
||||||
document.addEventListener('saved', function(evt){
|
document.addEventListener('saved', function(evt){
|
||||||
try {
|
try {
|
||||||
// Prefer toastr if available
|
// Prefer toastr if available
|
||||||
if (typeof toastr !== 'undefined' && toastr && typeof toastr.success === 'function') {
|
if (typeof toastr !== 'undefined' && toastr && typeof toastr.success === 'function') {
|
||||||
toastr.success('Saved');
|
toastr.success('Saved');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lightweight fallback toast
|
// Lightweight fallback toast
|
||||||
const id = 'htmx-save-toast';
|
const id = 'htmx-save-toast';
|
||||||
let el = document.getElementById(id);
|
let el = document.getElementById(id);
|
||||||
if (!el) {
|
if (!el) {
|
||||||
el = document.createElement('div');
|
el = document.createElement('div');
|
||||||
el.id = id;
|
el.id = id;
|
||||||
el.style.position = 'fixed';
|
el.style.position = 'fixed';
|
||||||
el.style.top = '1rem';
|
el.style.top = '1rem';
|
||||||
el.style.right = '1rem';
|
el.style.right = '1rem';
|
||||||
el.style.zIndex = 2000;
|
el.style.zIndex = 2000;
|
||||||
el.style.padding = '0.6rem 0.9rem';
|
el.style.padding = '0.6rem 0.9rem';
|
||||||
el.style.background = 'rgba(40,167,69,0.95)';
|
el.style.background = 'rgba(40,167,69,0.95)';
|
||||||
el.style.color = '#fff';
|
el.style.color = '#fff';
|
||||||
el.style.borderRadius = '0.4rem';
|
el.style.borderRadius = '0.4rem';
|
||||||
el.style.boxShadow = '0 2px 8px rgba(0,0,0,0.2)';
|
el.style.boxShadow = '0 2px 8px rgba(0,0,0,0.2)';
|
||||||
el.style.fontWeight = '600';
|
el.style.fontWeight = '600';
|
||||||
el.style.opacity = '0';
|
el.style.opacity = '0';
|
||||||
el.style.transition = 'opacity 220ms ease-in-out';
|
el.style.transition = 'opacity 220ms ease-in-out';
|
||||||
document.body.appendChild(el);
|
document.body.appendChild(el);
|
||||||
}
|
}
|
||||||
el.textContent = 'Saved';
|
el.textContent = 'Saved';
|
||||||
// show
|
// show
|
||||||
requestAnimationFrame(function(){ el.style.opacity = '1'; });
|
requestAnimationFrame(function(){ el.style.opacity = '1'; });
|
||||||
// hide after 1.6s
|
// hide after 1.6s
|
||||||
setTimeout(function(){ el.style.opacity = '0'; }, 1600);
|
setTimeout(function(){ el.style.opacity = '0'; }, 1600);
|
||||||
} catch (e) { console && console.error && console.error(e); }
|
} catch (e) { console && console.error && console.error(e); }
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -170,8 +170,15 @@
|
|||||||
if(hxTrigger && hxTrigger.indexOf('saved') !== -1){
|
if(hxTrigger && hxTrigger.indexOf('saved') !== -1){
|
||||||
handled = true;
|
handled = true;
|
||||||
if(window.htmx && typeof htmx.off === 'function') htmx.off('htmx:afterRequest', htmxHandler);
|
if(window.htmx && typeof htmx.off === 'function') htmx.off('htmx:afterRequest', htmxHandler);
|
||||||
document.getElementById('goto-button').value = destIndex;
|
try{
|
||||||
$('#goto-button').click();
|
const pageUrl = `{% if cid %}{% url 'physics:exam_take' pk=exam.pk sk=0 cid=cid passcode=passcode %}{% else %}{% url 'physics:exam_take_user' pk=exam.pk sk=0 %}{% endif %}`.replace('/0/', '/' + destIndex + '/');
|
||||||
|
window.location = pageUrl;
|
||||||
|
}catch(e){
|
||||||
|
if(document.getElementById('goto-button')){
|
||||||
|
document.getElementById('goto-button').value = destIndex;
|
||||||
|
$('#goto-button').click();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}catch(e){/* ignore */}
|
}catch(e){/* ignore */}
|
||||||
}
|
}
|
||||||
@@ -181,13 +188,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// As a fallback listen for the custom 'saved' event
|
// As a fallback listen for the custom 'saved' event
|
||||||
const onSavedFallback = function(){ if(handled) return; handled = true; document.removeEventListener('saved', onSavedFallback); document.getElementById('goto-button').value = destIndex; $('#goto-button').click(); };
|
const onSavedFallback = function(){ if(handled) return; handled = true; document.removeEventListener('saved', onSavedFallback); try{ const pageUrl = `{% if cid %}{% url 'physics:exam_take' pk=exam.pk sk=0 cid=cid passcode=passcode %}{% else %}{% url 'physics:exam_take_user' pk=exam.pk sk=0 %}{% endif %}`.replace('/0/', '/' + destIndex + '/'); window.location = pageUrl; }catch(e){ if(document.getElementById('goto-button')){ document.getElementById('goto-button').value = destIndex; $('#goto-button').click(); } } };
|
||||||
document.addEventListener('saved', onSavedFallback);
|
document.addEventListener('saved', onSavedFallback);
|
||||||
|
|
||||||
tmp.click();
|
tmp.click();
|
||||||
|
|
||||||
// final timeout fallback
|
// final timeout fallback
|
||||||
setTimeout(function(){ if(!handled){ try{ document.removeEventListener('saved', onSavedFallback); if(window.htmx && typeof htmx.off === 'function') htmx.off('htmx:afterRequest', htmxHandler); document.getElementById('goto-button').value = destIndex; $('#goto-button').click(); }catch(e){} } }, 1500);
|
setTimeout(function(){ if(!handled){ try{ document.removeEventListener('saved', onSavedFallback); if(window.htmx && typeof htmx.off === 'function') htmx.off('htmx:afterRequest', htmxHandler); const pageUrl = `{% if cid %}{% url 'physics:exam_take' pk=exam.pk sk=0 cid=cid passcode=passcode %}{% else %}{% url 'physics:exam_take_user' pk=exam.pk sk=0 %}{% endif %}`.replace('/0/', '/' + destIndex + '/'); window.location = pageUrl; }catch(e){ try{ if(document.getElementById('goto-button')){ document.getElementById('goto-button').value = destIndex; $('#goto-button').click(); } }catch(e){} } } }, 1500);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user