Merge pull request #782 from ae-utbm/ajax-navigation-history

Ajax navigation history in uv guide
This commit is contained in:
thomas girod 2024-08-26 22:29:19 +02:00 committed by GitHub
commit 68d0a16d1c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 110 additions and 53 deletions

View File

@ -74,6 +74,7 @@ const initialUrlParams = new URLSearchParams(window.location.search);
* @enum {number}
*/
const History = {
NONE: 0,
PUSH: 1,
REPLACE: 2,
};
@ -82,9 +83,12 @@ const History = {
* @param {string} key
* @param {string | string[] | null} value
* @param {History} action
* @param {URL | null} url
*/
function update_query_string(key, value, action = History.REPLACE) {
const url = new URL(window.location.href);
function update_query_string(key, value, action = History.REPLACE, url = null) {
if (!url){
url = new URL(window.location.href);
}
if (!value) {
// If the value is null, undefined or empty => delete it
url.searchParams.delete(key)
@ -96,8 +100,10 @@ function update_query_string(key, value, action = History.REPLACE) {
}
if (action === History.PUSH) {
history.pushState(null, document.title, url.toString());
history.pushState(null, "", url.toString());
} else if (action === History.REPLACE) {
history.replaceState(null, document.title, url.toString());
history.replaceState(null, "", url.toString());
}
return url;
}

View File

@ -116,6 +116,46 @@
{% endif %}
{% endmacro %}
{% macro paginate_alpine(page, nb_pages) %}
{# Add pagination buttons for ajax based content with alpine
Notes:
This can only be used in the scope of your alpine datastore
Notes:
You might need to listen to the "popstate" event in your code
to update the current page you are on when the user goes back in
it's browser history with the back arrow
Parameters:
page (str): name of the alpine page variable in your datastore
nb_page (str): call to a javascript function or variable returning
the maximum number of pages to paginate
#}
<nav class="pagination" x-show="{{ nb_pages }} > 1">
{# Adding the prevent here is important, because otherwise,
clicking on the pagination buttons could submit the picture management form
and reload the page #}
<button
@click.prevent="{{ page }}--"
:disabled="{{ page }} <= 1"
@keyup.right.window="{{ page }} = Math.min({{ nb_pages }}, {{ page }} + 1)"
>
<i class="fa fa-caret-left"></i>
</button>
<template x-for="i in {{ nb_pages }}">
<button x-text="i" @click.prevent="{{ page }} = i" :class="{active: {{ page }} === i}"></button>
</template>
<button
@click.prevent="{{ page }}++"
:disabled="{{ page }} >= {{ nb_pages }}"
@keyup.left.window="{{ page }} = Math.max(1, {{ page }} - 1)"
>
<i class="fa fa-caret-right"></i>
</button>
</nav>
{% endmacro %}
{% macro paginate(page_obj, paginator, js_action) %}
{% set js = js_action|default('') %}
{% if page_obj.has_previous() or page_obj.has_next() %}

View File

@ -1,4 +1,5 @@
{% extends "core/base.jinja" %}
{% from 'core/macros.jinja' import paginate_alpine %}
{% block title %}
{% trans %}UV Guide{% endtrans %}
@ -113,17 +114,7 @@
</template>
</tbody>
</table>
<nav class="pagination" x-show="max_page() > 1">
<button @click="page--" :disabled="page <= 1">
<i class="fa fa-caret-left"></i>
</button>
<template x-for="i in max_page()">
<button x-text="i" @click="page = i" :class="(page === i) && 'active'"></button>
</template>
<button @click="page++" :disabled="page >= max_page()">
<i class="fa fa-caret-right"></i>
</button>
</nav>
{{ paginate_alpine("page", "max_page()") }}
</div>
<script>
{#
@ -141,36 +132,76 @@
Alpine.data("uv_search", () => ({
uvs: [],
loading: false,
page: parseInt(initialUrlParams.get("page")) || page_default,
page_size: parseInt(initialUrlParams.get("page_size")) || page_size_default,
search: initialUrlParams.get("search") || "",
department: initialUrlParams.getAll("department"),
credit_type: initialUrlParams.getAll("credit_type"),
{# The semester is easier to use on the backend as an enum (spring/autumn/both/none)
and easier to use on the frontend as an array ([spring, autumn]).
Thus there is some conversion involved when both communicate together #}
semester: initialUrlParams.has("semester") ?
initialUrlParams.get("semester").split("_AND_") : [],
page: page_default,
page_size: page_size_default,
search: "",
department: [],
credit_type: [],
semester: [],
to_change: [],
pushstate: History.PUSH,
update: undefined,
async initialize_args() {
let url = new URLSearchParams(window.location.search);
this.pushstate = History.REPLACE;
this.page = parseInt(url.get("page")) || page_default;;
this.page_size = parseInt(url.get("page_size")) || page_size_default;
this.search = url.get("search") || "";
this.department = url.getAll("department");
this.credit_type = url.getAll("credit_type");
{# The semester is easier to use on the backend as an enum (spring/autumn/both/none)
and easier to use on the frontend as an array ([spring, autumn]).
Thus there is some conversion involved when both communicate together #}
this.semester = url.has("semester") ?
url.get("semester").split("_AND_") : [];
this.update()
},
async init() {
this.update = Alpine.debounce(async () => {
{# Create the whole url before changing everything all at once #}
let first = this.to_change.shift();
let url = update_query_string(first.param, first.value, History.NONE);
this.to_change.forEach((value) => {
url = update_query_string(value.param, value.value, History.NONE, url);
})
update_query_string(first.param, first.value, this.pushstate, url);
await this.fetch_data(); {# reload data on form change #}
this.to_change = [];
this.pushstate = History.PUSH;
}, 50);
let search_params = ["search", "department", "credit_type", "semester"];
let pagination_params = ["semester", "page"];
let pagination_params = ["page", "page_size"];
search_params.forEach((param) => {
this.$watch(param, async () => {
{# Reset pagination on search #}
this.$watch(param, async (value) => {
if (this.pushstate != History.PUSH){
{# This means that we are doing a mass param edit #}
return;
}
{# Reset pagination on search #}
this.page = page_default;
this.page_size = page_size_default;
});
});
search_params.concat(pagination_params).forEach((param) => {
this.$watch(param, async (value) => {
update_query_string(param, value);
await this.fetch_data(); {# reload data on form change #}
this.to_change.push({ param: param, value: value })
this.update();
});
});
await this.fetch_data(); {# load initial data #}
window.addEventListener("popstate", async (event) => {
await this.initialize_args();
});
await this.initialize_args();
},
async fetch_data() {
this.loading = true;
const url = "{{ url("api:fetch_uvs") }}" + window.location.search;

View File

@ -1,4 +1,5 @@
{% extends "core/base.jinja" %}
{% from 'core/macros.jinja' import paginate_alpine %}
{%- block additional_css -%}
<link rel="stylesheet" href="{{ scss('sas/album.scss') }}">
@ -83,28 +84,7 @@
</a>
</template>
</div>
<nav class="pagination" x-show="nb_pages() > 1">
{# Adding the prevent here is important, because otherwise,
clicking on the pagination buttons could submit the picture management form
and reload the page #}
<button
@click.prevent="page--"
:disabled="page <= 1"
@keyup.right.window="page = Math.min(nb_pages(), page + 1)"
>
<i class="fa fa-caret-left"></i>
</button>
<template x-for="i in nb_pages()">
<button x-text="i" @click.prevent="page = i" :class="{active: page === i}"></button>
</template>
<button
@click.prevent="page++"
:disabled="page >= nb_pages()"
@keyup.left.window="page = Math.max(1, page - 1)"
>
<i class="fa fa-caret-right"></i>
</button>
</nav>
{{ paginate_alpine("page", "nb_pages()") }}
</div>
{% if is_sas_admin %}