forms: allow clicking back to any previous page (#11249) #1115

Merged
fpeters merged 1 commits from wip/11249-go-to-any-old-page into main 2024-03-15 07:32:48 +01:00
4 changed files with 81 additions and 6 deletions

View File

@ -1282,7 +1282,7 @@ def test_form_multi_page_page_name_as_title(pub):
next_page = next_page.forms[0].submit('submit')
assert_current_page(next_page, 'Validating')
assert 'Check values then click submit.' in next_page.text
assert next_page.text.count('1st page') == 2 # in steps and in main body
assert next_page.text.count('1st page') == 3 # in steps (twice) and in main body
# add a comment that will not be displayed and should therefore not be
# considered.
@ -1305,7 +1305,48 @@ def test_form_multi_page_page_name_as_title(pub):
next_page = next_page.forms[0].submit('submit')
assert_current_page(next_page, 'Validating')
assert 'Check values then click submit.' in next_page.text
assert next_page.text.count('1st page') == 2 # in steps and in main body
assert next_page.text.count('1st page') == 3 # in steps (twice) and in main body
def test_form_multi_page_go_back(pub):
formdef = create_formdef()
formdef.fields = [
fields.PageField(id='0', label='1st page'),
fields.StringField(id='1', label='string'),
fields.PageField(id='2', label='2nd page'),
fields.PageField(id='3', label='3rd page'),
fields.StringField(id='4', label='string 2'),
]
formdef.store()
formdef.data_class().wipe()
resp = get_app(pub).get('/test/')
resp.forms[0]['f1'] = 'foo'
resp = resp.forms[0].submit('submit') # -> 2nd page
assert_current_page(resp, '2nd page')
resp = resp.forms[0].submit('submit') # -> 3rd page
assert_current_page(resp, '3rd page')
resp.forms[0]['f4'] = 'foo'
resp = resp.forms[0].submit('submit') # -> validation page
assert_current_page(resp, 'Validating')
# go back to second page (javascript would set this)
resp.forms[0]['previous-page-id'] = '2'
resp = resp.forms[0].submit('previous')
assert_current_page(resp, '2nd page')
resp = resp.forms[0].submit('submit') # -> 3rd page
# go back to first page (javascript would set this)
resp.forms[0]['previous-page-id'] = '0'
resp = resp.forms[0].submit('previous')
assert_current_page(resp, '1st page')
resp = resp.forms[0].submit('submit') # -> 2nd page
resp = resp.forms[0].submit('submit') # -> 3rd page
# go back to invalid page (javascript would not set this)
resp.forms[0]['previous-page-id'] = '10'
resp = resp.forms[0].submit('previous')
assert_current_page(resp, '1st page') # fallback to first page
def test_form_submit_with_user(pub, emails):

View File

@ -422,6 +422,7 @@ class FormPage(Directory, TempfileDirectoryMixin, FormTemplateMixin):
'current_page_index': current_position,
'current_page_no': current_position, # legacy, for themes
'page_labels': page_labels,
'pages': self.pages,
}
def step(self):
@ -1013,6 +1014,7 @@ class FormPage(Directory, TempfileDirectoryMixin, FormTemplateMixin):
# if there's a form with a single page (at all, not as the result of conditions),
# and no confirmation page, add native quixote CSRF protection.
form.add(FormTokenWidget, form.TOKEN_NAME)
form.add_hidden('previous-page-id', '')
form.attrs['data-live-url'] = self.formdef.get_url(language=get_publisher().current_language) + 'live'
form.attrs['data-live-validation-url'] = (
self.formdef.get_url(language=get_publisher().current_language) + 'live-validation'
@ -1027,7 +1029,9 @@ class FormPage(Directory, TempfileDirectoryMixin, FormTemplateMixin):
return form
def create_view_form(self, *args, **kwargs):
return self.formdef.create_view_form(*args, **kwargs)
form = self.formdef.create_view_form(*args, **kwargs)
form.add_hidden('previous-page-id', '')
return form
def check_authentication_context(self):
if not self.formdef.required_authentication_contexts:
@ -1595,8 +1599,14 @@ class FormPage(Directory, TempfileDirectoryMixin, FormTemplateMixin):
def previous_page(self, page_no, magictoken):
try:
new_page_no = page_no - 1
previous_page = self.pages[new_page_no]
previous_page_id = get_request().form.get('previous-page-id')
if previous_page_id:
new_page_no, previous_page = [
x for x in enumerate(self.pages[:page_no]) if x[1].id == previous_page_id
][0]
else:
new_page_no = page_no - 1
previous_page = self.pages[new_page_no]
except IndexError:
new_page_no = 0
previous_page = self.pages[0]

View File

@ -1083,3 +1083,20 @@ document.addEventListener('DOMContentLoaded', function(){
initLiveValidation(blockWidgets)

S'il n'y a pas l'<input type=hidden> on ne fait rien, ça permettrait d'ajouter un feature flag pour désactiver ça, mais pour le moment ça me va de faire ça sans feature flag.

S'il n'y a pas l'`<input type=hidden>` on ne fait rien, ça permettrait d'ajouter un feature flag pour désactiver ça, mais pour le moment ça me va de faire ça sans feature flag.
})
})
document.addEventListener('DOMContentLoaded', function(){
const previous_page_id_input = document.querySelector('[name="previous-page-id"]')
if (!previous_page_id_input) return

Pour la navigation au clavier, s'il y a enter ou un espace on remplit l'<input type=hidden> et on "click" sur le bouton précédent.

Pour la navigation au clavier, s'il y a enter ou un espace on remplit l'`<input type=hidden>` et on "click" sur le bouton précédent.
document.querySelectorAll('.wcs-step[data-page-id]').forEach((step, idx) => {
step.addEventListener('keydown', function(e) {
if (e.key !== "Enter" && e.key !== " ") return
e.preventDefault()

Même chose sur un clic.

Même chose sur un clic.
previous_page_id_input.value = step.dataset.pageId
document.querySelector('button[name="previous"]').dispatchEvent(new MouseEvent('click'))
})
step.addEventListener('click', function() {
previous_page_id_input.value = step.dataset.pageId
document.querySelector('button[name="previous"]').dispatchEvent(new MouseEvent('click'))
})
})
})

View File

@ -17,7 +17,14 @@
{% if forloop.last %}last{% endif %}
{% if forloop.counter == current_page_no %}current{% endif %}
{% if forloop.counter < current_page_no %}step-before{% endif %}
{% if forloop.counter > current_page_no %}step-after{% endif %}" >
{% if forloop.counter > current_page_no %}step-after{% endif %}"
{% if forloop.counter < current_page_no %}
aria-label="{% blocktrans with page_label=page_label %}Go back to step: {{ page_label }}{% endblocktrans %}"
role="button"
tabindex="0"

Avec le js plus haut, ces attributs assurent normalement tout le nécessaire pour l'accessibilité au clavier.

Avec le js plus haut, ces attributs assurent normalement tout le nécessaire pour l'accessibilité au clavier.
data-page-id="{% with page=pages|get:forloop.counter0 %}{{ page.id }}{% endwith %}"
{% endif %}
>
<abbr
aria-label="{% blocktrans with page_no=forloop.counter page_count=page_labels|length %}Step {{ page_no }} of {{ page_count }}:{% endblocktrans %}"
title="{% blocktrans with page_no=forloop.counter page_count=page_labels|length %}Step {{ page_no }} of {{ page_count }}{% endblocktrans %}"