django-spire 0.22.4__py3-none-any.whl → 0.23.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_spire/auth/group/forms.py +3 -4
- django_spire/auth/group/utils.py +1 -2
- django_spire/auth/group/views/form_views.py +22 -14
- django_spire/auth/group/views/page_views.py +1 -1
- django_spire/auth/mfa/utils.py +1 -1
- django_spire/auth/permissions/consts.py +2 -1
- django_spire/auth/permissions/decorators.py +1 -2
- django_spire/auth/permissions/permissions.py +1 -3
- django_spire/comment/factories.py +1 -1
- django_spire/comment/mixins.py +1 -1
- django_spire/comment/views.py +1 -1
- django_spire/consts.py +1 -1
- django_spire/contrib/breadcrumb/breadcrumbs.py +1 -1
- django_spire/contrib/form/confirmation_forms.py +1 -1
- django_spire/contrib/form/utils.py +26 -16
- django_spire/contrib/generic_views/modal_views.py +1 -1
- django_spire/contrib/generic_views/portal_views.py +18 -55
- django_spire/contrib/ordering/validators.py +4 -8
- django_spire/contrib/queryset/enums.py +2 -3
- django_spire/contrib/queryset/mixins.py +17 -6
- django_spire/contrib/session/controller.py +0 -1
- django_spire/core/context_processors.py +1 -1
- django_spire/core/management/commands/spire_startapp_pkg/user_input.py +1 -1
- django_spire/core/middleware/maintenance.py +2 -1
- django_spire/core/middleware.py +2 -1
- django_spire/core/redirect/generic_redirect.py +1 -1
- django_spire/core/redirect/safe_redirect.py +2 -1
- django_spire/core/shortcuts.py +4 -2
- django_spire/core/table/__init__.py +0 -0
- django_spire/core/table/enums.py +18 -0
- django_spire/core/templates/django_spire/card/infinite_scroll_card.html +3 -137
- django_spire/core/templates/django_spire/container/infinite_scroll_container.html +64 -0
- django_spire/core/templates/django_spire/infinite_scroll/base.html +348 -0
- django_spire/core/templates/django_spire/infinite_scroll/element/footer.html +11 -0
- django_spire/core/templates/django_spire/infinite_scroll/scroll.html +142 -0
- django_spire/core/templates/django_spire/item/infinite_scroll_item.html +33 -0
- django_spire/core/templates/django_spire/lazy_tab/element/lazy_tab_section_element.html +19 -0
- django_spire/core/templates/django_spire/lazy_tab/element/lazy_tab_trigger_element.html +15 -0
- django_spire/core/templates/django_spire/lazy_tab/lazy_tab.html +157 -0
- django_spire/core/templates/django_spire/page/infinite_scroll_list_page.html +7 -0
- django_spire/core/templates/django_spire/table/base.html +185 -373
- django_spire/core/templates/django_spire/table/element/footer.html +7 -15
- django_spire/core/templates/django_spire/table/element/header.html +1 -1
- django_spire/core/templates/django_spire/table/element/row.html +15 -7
- django_spire/core/templatetags/spire_core_tags.py +1 -2
- django_spire/file/fields.py +1 -1
- django_spire/file/interfaces.py +1 -1
- django_spire/file/views.py +1 -1
- django_spire/history/activity/utils.py +1 -1
- django_spire/history/mixins.py +0 -4
- django_spire/notification/app/context_data.py +3 -1
- django_spire/notification/app/views/json_views.py +1 -1
- django_spire/notification/app/views/page_views.py +2 -1
- django_spire/notification/app/views/template_views.py +2 -2
- django_spire/profiling/middleware/profiling.py +2 -2
- django_spire/profiling/panel.py +2 -2
- django_spire/testing/__init__.py +0 -0
- django_spire/testing/playwright/__init__.py +64 -0
- django_spire/testing/playwright/components/__init__.py +45 -0
- django_spire/testing/playwright/components/accordion.py +55 -0
- django_spire/testing/playwright/components/attribute_element.py +73 -0
- django_spire/testing/playwright/components/base_session_filter_form.py +57 -0
- django_spire/testing/playwright/components/breadcrumb_element.py +56 -0
- django_spire/testing/playwright/components/card.py +102 -0
- django_spire/testing/playwright/components/dropdown.py +87 -0
- django_spire/testing/playwright/components/infinite_scroll.py +158 -0
- django_spire/testing/playwright/components/lazy_tab.py +92 -0
- django_spire/testing/playwright/components/modal.py +101 -0
- django_spire/testing/playwright/components/navigation.py +119 -0
- django_spire/testing/playwright/components/notification_bell.py +59 -0
- django_spire/testing/playwright/components/theme_selector.py +46 -0
- django_spire/testing/playwright/components/toast.py +72 -0
- django_spire/testing/playwright/fixtures.py +54 -0
- django_spire/testing/playwright/pages/__init__.py +6 -0
- django_spire/testing/playwright/pages/base.py +24 -0
- django_spire/theme/models.py +1 -1
- django_spire/theme/tests/test_context_processor.py +0 -1
- django_spire/theme/views/json_views.py +1 -1
- django_spire/theme/views/page_views.py +1 -1
- {django_spire-0.22.4.dist-info → django_spire-0.23.2.dist-info}/METADATA +4 -1
- {django_spire-0.22.4.dist-info → django_spire-0.23.2.dist-info}/RECORD +84 -54
- {django_spire-0.22.4.dist-info → django_spire-0.23.2.dist-info}/WHEEL +0 -0
- {django_spire-0.22.4.dist-info → django_spire-0.23.2.dist-info}/licenses/LICENSE.md +0 -0
- {django_spire-0.22.4.dist-info → django_spire-0.23.2.dist-info}/top_level.txt +0 -0
|
@@ -1,141 +1,7 @@
|
|
|
1
1
|
{% extends 'django_spire/card/title_card.html' %}
|
|
2
2
|
|
|
3
|
-
{% block
|
|
4
|
-
<div
|
|
5
|
-
x-data="{
|
|
6
|
-
card_title_dropdown: false,
|
|
7
|
-
endpoint: '{{ endpoint }}',
|
|
8
|
-
shared_payload: {},
|
|
9
|
-
page_size: parseInt('{{ page_size }}' || 10),
|
|
10
|
-
current_page: parseInt('{{ current_page }}' || 1),
|
|
11
|
-
has_next: {{ has_next|default:'false'|yesno:'true,false' }} || false,
|
|
12
|
-
is_loading: false,
|
|
13
|
-
observer: null,
|
|
3
|
+
{% block card_title %}{{ card_title }}{% endblock %}
|
|
14
4
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if (this.$refs.scroll_container) {
|
|
18
|
-
this.$refs.scroll_container.scrollTop = 0;
|
|
19
|
-
}
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
if (this.has_next) {
|
|
23
|
-
await this.setup_observer();
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
this.shared_payload = JSON.parse(document.getElementById('shared_payload').textContent)
|
|
27
|
-
},
|
|
28
|
-
|
|
29
|
-
async setup_observer() {
|
|
30
|
-
let trigger = this.$refs.infinite_scroll_trigger;
|
|
31
|
-
|
|
32
|
-
if (!trigger) {
|
|
33
|
-
console.error('Infinite scroll trigger element not found.');
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
let options = {
|
|
38
|
-
root: null,
|
|
39
|
-
rootMargin: '0px',
|
|
40
|
-
threshold: 1.0
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
this.observer = new IntersectionObserver(
|
|
44
|
-
(entries) => {
|
|
45
|
-
entries.forEach(async entry => {
|
|
46
|
-
if (entry.isIntersecting && this.has_next && !this.is_loading) {
|
|
47
|
-
await this.load_more();
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
},
|
|
51
|
-
options
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
this.observer.observe(trigger);
|
|
55
|
-
},
|
|
56
|
-
|
|
57
|
-
async fetch(next_page) {
|
|
58
|
-
if (!this.endpoint) return { success: false, error: 'No endpoint provided' };
|
|
59
|
-
|
|
60
|
-
let params = new URLSearchParams({
|
|
61
|
-
page: next_page,
|
|
62
|
-
page_size: this.page_size
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
let url = `${this.endpoint}?${params}`;
|
|
66
|
-
let view = new ViewGlue(url, this.shared_payload);
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
await view.render_insert_adjacent(this.$refs.item_container, {}, 'beforeend')
|
|
70
|
-
return { success: true };
|
|
71
|
-
} catch (error) {
|
|
72
|
-
console.error(error);
|
|
73
|
-
return { success: false, error };
|
|
74
|
-
}
|
|
75
|
-
},
|
|
76
|
-
|
|
77
|
-
async load_more() {
|
|
78
|
-
if (!this.endpoint) return;
|
|
79
|
-
|
|
80
|
-
this.is_loading = true;
|
|
81
|
-
|
|
82
|
-
let next_page = this.current_page + 1;
|
|
83
|
-
|
|
84
|
-
let container = this.$refs.item_container;
|
|
85
|
-
let previous_count = container.childElementCount;
|
|
86
|
-
|
|
87
|
-
let result = await this.fetch(next_page);
|
|
88
|
-
|
|
89
|
-
if (!result.success) {
|
|
90
|
-
this.is_loading = false;
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
let current_count = container.childElementCount;
|
|
95
|
-
let added = current_count - previous_count;
|
|
96
|
-
|
|
97
|
-
if (added > 0) {
|
|
98
|
-
this.current_page = next_page;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (added < this.page_size) {
|
|
102
|
-
this.has_next = false;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
this.is_loading = false;
|
|
106
|
-
},
|
|
107
|
-
|
|
108
|
-
toggle_card_title_dropdown() {
|
|
109
|
-
this.card_title_dropdown = !this.card_title_dropdown;
|
|
110
|
-
}
|
|
111
|
-
}"
|
|
112
|
-
>
|
|
113
|
-
{{ shared_payload|json_script:'shared_payload' }}
|
|
114
|
-
|
|
115
|
-
<div class="row justify-content-between align-items-center {% block card_title_class %}mb-2 pb-2 border-bottom-secondary{% endblock %}">
|
|
116
|
-
<div class="col">
|
|
117
|
-
<div class="card-title text-uppercase mb-0">{% block card_title %}{% endblock %}</div>
|
|
118
|
-
</div>
|
|
119
|
-
<div class="col-auto d-flex align-items-center" style="min-height: 2.0rem;">
|
|
120
|
-
{% block card_button %}
|
|
121
|
-
{% endblock %}
|
|
122
|
-
</div>
|
|
123
|
-
</div>
|
|
124
|
-
|
|
125
|
-
<div
|
|
126
|
-
x-ref="scroll_container"
|
|
127
|
-
style="{% block card_title_content_style %}{% endblock %} overscroll-behavior: contain;"
|
|
128
|
-
>
|
|
129
|
-
{% block card_title_content %}
|
|
130
|
-
{% endblock %}
|
|
131
|
-
|
|
132
|
-
<div x-ref="item_container"></div>
|
|
133
|
-
|
|
134
|
-
<div x-show="is_loading" class="text-center my-3">
|
|
135
|
-
<div class="spinner-border" role="status"></div>
|
|
136
|
-
</div>
|
|
137
|
-
|
|
138
|
-
<div x-ref="infinite_scroll_trigger" style="height:10px;"></div>
|
|
139
|
-
</div>
|
|
140
|
-
</div>
|
|
5
|
+
{% block card_title_content %}
|
|
6
|
+
{% include 'django_spire/infinite_scroll/scroll.html' with scroll_height=scroll_height|default:'300px' %}
|
|
141
7
|
{% endblock %}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{% extends 'django_spire/infinite_scroll/base.html' %}
|
|
2
|
+
|
|
3
|
+
{% block scroll_toolbar %}
|
|
4
|
+
<div class="row {% block container_outer_class %}{% endblock %}">
|
|
5
|
+
<div class="col-12">
|
|
6
|
+
<div class="row align-items-end justify-content-between px-md-3">
|
|
7
|
+
<div class="col">
|
|
8
|
+
<h1 class="fs-1 fw-semi-bold">
|
|
9
|
+
{% block container_title %}{% endblock %}
|
|
10
|
+
</h1>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<div class="col-auto">
|
|
14
|
+
{% block container_button %}{% endblock %}
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
{% block container_filter_section %}{% endblock %}
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
{% endblock %}
|
|
22
|
+
|
|
23
|
+
{% block scroll_container %}
|
|
24
|
+
<div class="row {% block container_class %}{% endblock %}">
|
|
25
|
+
<div class="col-12 {% block container_content_class %}{% endblock %}">
|
|
26
|
+
<div class="position-relative mt-3">
|
|
27
|
+
<div
|
|
28
|
+
class="p-3"
|
|
29
|
+
style="height: calc(100vh - {{ container_offset|default:'350' }}px); overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch;"
|
|
30
|
+
x-ref="scroll_container"
|
|
31
|
+
>
|
|
32
|
+
<div x-ref="content_container">
|
|
33
|
+
{% block container_content %}{% endblock %}
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div style="height: 10px;" x-ref="infinite_scroll_trigger"></div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<template x-if="show_loading && !is_refreshing">
|
|
40
|
+
<div
|
|
41
|
+
class="position-absolute d-flex justify-content-center align-items-center"
|
|
42
|
+
style="top: 0; left: 0; right: 0; bottom: 0; background: color-mix(in srgb, var(--app-layer-one) 85%, transparent);"
|
|
43
|
+
>
|
|
44
|
+
<div class="spinner-border text-app-primary" role="status"></div>
|
|
45
|
+
</div>
|
|
46
|
+
</template>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
{% endblock %}
|
|
51
|
+
|
|
52
|
+
{% block scroll_footer %}
|
|
53
|
+
<div class="row mb-4 mt-3 px-md-3">
|
|
54
|
+
<div class="col text-start">
|
|
55
|
+
<span class="fs-7 text-app-secondary">
|
|
56
|
+
Showing <span x-text="loaded_count"></span> of <span x-text="total_count"></span> items
|
|
57
|
+
</span>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div class="col-auto text-end">
|
|
61
|
+
{% block footer_container_action %}{% endblock %}
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
{% endblock %}
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
<div
|
|
2
|
+
x-data="{
|
|
3
|
+
batch_size: parseInt('{{ batch_size|default:25 }}'),
|
|
4
|
+
current_page: parseInt('{{ current_page|default:0 }}'),
|
|
5
|
+
endpoint: '{{ endpoint }}',
|
|
6
|
+
has_next: {{ has_next|default:'true'|yesno:'true,false' }},
|
|
7
|
+
shared_payload: {},
|
|
8
|
+
sort_column: {% if sort_column %}'{{ sort_column }}'{% else %}null{% endif %},
|
|
9
|
+
sort_direction: '{{ sort_direction|default:'asc' }}',
|
|
10
|
+
total_count: parseInt('{{ total_count|default:0 }}'),
|
|
11
|
+
|
|
12
|
+
average_item_height: 0,
|
|
13
|
+
is_loading: false,
|
|
14
|
+
is_refreshing: false,
|
|
15
|
+
loaded_count: 0,
|
|
16
|
+
loading_timer: null,
|
|
17
|
+
observer: null,
|
|
18
|
+
prevent_auto_load: false,
|
|
19
|
+
show_loading: false,
|
|
20
|
+
skeleton_count: 0,
|
|
21
|
+
|
|
22
|
+
{% block scroll_xdata %}{% endblock %}
|
|
23
|
+
|
|
24
|
+
async init() {
|
|
25
|
+
await this.$nextTick();
|
|
26
|
+
|
|
27
|
+
this.reset_scroll_position();
|
|
28
|
+
this.load_shared_payload();
|
|
29
|
+
|
|
30
|
+
if (this.should_load_initial_data()) {
|
|
31
|
+
this.skeleton_count = this.batch_size;
|
|
32
|
+
await this.load_more();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (this.has_next) {
|
|
36
|
+
setTimeout(() => this.setup_observer(), 500);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
{% block scroll_init %}{% endblock %}
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
build_request_params(next_page) {
|
|
43
|
+
let params = new URLSearchParams({
|
|
44
|
+
page: next_page,
|
|
45
|
+
batch_size: this.batch_size
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (this.sort_column) {
|
|
49
|
+
params.set('sort', this.sort_column);
|
|
50
|
+
params.set('direction', this.sort_direction);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
{% block scroll_build_params %}{% endblock %}
|
|
54
|
+
|
|
55
|
+
return params;
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
async check_container_height() {
|
|
59
|
+
let scroll_container = this.$refs.scroll_container;
|
|
60
|
+
let trigger = this.$refs.infinite_scroll_trigger;
|
|
61
|
+
|
|
62
|
+
if (!scroll_container || !trigger || !this.has_next || this.is_refreshing || this.prevent_auto_load || this.is_loading) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
67
|
+
|
|
68
|
+
let container_rect = scroll_container.getBoundingClientRect();
|
|
69
|
+
let trigger_rect = trigger.getBoundingClientRect();
|
|
70
|
+
|
|
71
|
+
if (trigger_rect.top <= container_rect.bottom) {
|
|
72
|
+
await this.load_more();
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
cleanup_loading_state() {
|
|
77
|
+
if (this.loading_timer) {
|
|
78
|
+
clearTimeout(this.loading_timer);
|
|
79
|
+
this.loading_timer = null;
|
|
80
|
+
}
|
|
81
|
+
this.is_loading = false;
|
|
82
|
+
this.show_loading = false;
|
|
83
|
+
this.skeleton_count = 0;
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
disconnect_observer() {
|
|
87
|
+
if (this.observer) {
|
|
88
|
+
this.observer.disconnect();
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
async fetch_items(next_page) {
|
|
93
|
+
if (!this.endpoint) {
|
|
94
|
+
return { success: false, error: 'No endpoint provided' };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let params = this.build_request_params(next_page);
|
|
98
|
+
let url = `${this.endpoint}?${params}`;
|
|
99
|
+
let view = new ViewGlue(url, this.shared_payload);
|
|
100
|
+
|
|
101
|
+
if (next_page === 1) {
|
|
102
|
+
this.$refs.content_container.innerHTML = '';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let previous_count = this.loaded_count;
|
|
106
|
+
|
|
107
|
+
await view.render_insert_adjacent(this.$refs.content_container, {}, 'beforeend');
|
|
108
|
+
|
|
109
|
+
let added = this.loaded_count - previous_count;
|
|
110
|
+
|
|
111
|
+
return { success: true, added: added };
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
get_sort_icon(column) {
|
|
115
|
+
if (this.sort_column !== column) {
|
|
116
|
+
return 'bi-chevron-expand';
|
|
117
|
+
}
|
|
118
|
+
return this.sort_direction === 'asc' ? 'bi-chevron-up' : 'bi-chevron-down';
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
handle_item_mounted(event) {
|
|
122
|
+
{% block handle_item_mounted %}
|
|
123
|
+
this.loaded_count++;
|
|
124
|
+
|
|
125
|
+
if (event.detail.item_element && this.average_item_height === 0) {
|
|
126
|
+
this.average_item_height = event.detail.item_element.offsetHeight;
|
|
127
|
+
}
|
|
128
|
+
{% endblock %}
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
handle_item_cleared() {
|
|
132
|
+
{% block handle_item_cleared %}
|
|
133
|
+
this.loaded_count = 0;
|
|
134
|
+
{% endblock %}
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
handle_total_count_updated(event) {
|
|
138
|
+
{% block handle_total_count_updated %}
|
|
139
|
+
this.total_count = event.detail.total_count;
|
|
140
|
+
|
|
141
|
+
if (event.detail.batch_size) {
|
|
142
|
+
this.batch_size = event.detail.batch_size;
|
|
143
|
+
}
|
|
144
|
+
{% endblock %}
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
async load_more() {
|
|
148
|
+
if (!this.endpoint || this.is_loading) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
this.is_loading = true;
|
|
153
|
+
this.skeleton_count = this.batch_size;
|
|
154
|
+
|
|
155
|
+
this.loading_timer = setTimeout(() => {
|
|
156
|
+
this.show_loading = true;
|
|
157
|
+
}, 200);
|
|
158
|
+
|
|
159
|
+
if (!this.$refs.content_container) {
|
|
160
|
+
this.cleanup_loading_state();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let result = await this.fetch_items(this.current_page + 1);
|
|
165
|
+
|
|
166
|
+
if (!result.success) {
|
|
167
|
+
this.cleanup_loading_state();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (result.added > 0) {
|
|
172
|
+
this.current_page++;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (result.added < this.batch_size) {
|
|
176
|
+
this.has_next = false;
|
|
177
|
+
this.disconnect_observer();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
this.cleanup_loading_state();
|
|
181
|
+
|
|
182
|
+
await this.$nextTick();
|
|
183
|
+
await this.check_container_height();
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
load_shared_payload() {
|
|
187
|
+
if (this.$refs.shared_payload) {
|
|
188
|
+
this.shared_payload = JSON.parse(this.$refs.shared_payload.textContent);
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
async refresh() {
|
|
193
|
+
if (this.is_refreshing || this.is_loading) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
this.disconnect_observer();
|
|
198
|
+
|
|
199
|
+
this.prevent_auto_load = true;
|
|
200
|
+
this.is_refreshing = true;
|
|
201
|
+
this.show_loading = true;
|
|
202
|
+
|
|
203
|
+
await this.$nextTick();
|
|
204
|
+
|
|
205
|
+
let target_count = this.loaded_count;
|
|
206
|
+
let target_pages = target_count > 0 ? Math.ceil(target_count / this.batch_size) : 1;
|
|
207
|
+
|
|
208
|
+
this.skeleton_count = target_count > 0 ? target_count : this.batch_size;
|
|
209
|
+
|
|
210
|
+
this.reset_state();
|
|
211
|
+
|
|
212
|
+
let pages_to_fetch = Math.max(1, target_pages);
|
|
213
|
+
let should_continue = true;
|
|
214
|
+
|
|
215
|
+
for (let page = 1; page <= pages_to_fetch && should_continue; page++) {
|
|
216
|
+
let result = await this.fetch_items(page);
|
|
217
|
+
|
|
218
|
+
if (!result.success) {
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (result.added > 0) {
|
|
223
|
+
this.current_page = page;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (result.added < this.batch_size) {
|
|
227
|
+
this.has_next = false;
|
|
228
|
+
should_continue = false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
await this.$nextTick();
|
|
233
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
234
|
+
|
|
235
|
+
this.is_refreshing = false;
|
|
236
|
+
this.show_loading = false;
|
|
237
|
+
this.skeleton_count = 0;
|
|
238
|
+
|
|
239
|
+
if (this.has_next) {
|
|
240
|
+
setTimeout(() => {
|
|
241
|
+
this.setup_observer();
|
|
242
|
+
|
|
243
|
+
setTimeout(() => {
|
|
244
|
+
this.prevent_auto_load = false;
|
|
245
|
+
}, 100);
|
|
246
|
+
}, 500);
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
reset_scroll_position() {
|
|
251
|
+
requestAnimationFrame(() => {
|
|
252
|
+
if (this.$refs.scroll_container) {
|
|
253
|
+
this.$refs.scroll_container.scrollTop = 0;
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
reset_state() {
|
|
259
|
+
this.current_page = 0;
|
|
260
|
+
this.has_next = true;
|
|
261
|
+
this.loaded_count = 0;
|
|
262
|
+
{% block scroll_reset_state %}{% endblock %}
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
async setup_observer() {
|
|
266
|
+
let trigger = this.$refs.infinite_scroll_trigger;
|
|
267
|
+
|
|
268
|
+
if (!trigger) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
let options = {
|
|
273
|
+
root: null,
|
|
274
|
+
rootMargin: '200px',
|
|
275
|
+
threshold: 0.01
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
this.observer = new IntersectionObserver(
|
|
279
|
+
(entries) => {
|
|
280
|
+
entries.forEach(async entry => {
|
|
281
|
+
if (entry.isIntersecting && this.has_next && !this.is_loading && !this.prevent_auto_load) {
|
|
282
|
+
await this.load_more();
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
},
|
|
286
|
+
options
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
this.observer.observe(trigger);
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
should_load_initial_data() {
|
|
293
|
+
return this.loaded_count === 0 && this.current_page === 0;
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
async sort_by(column) {
|
|
297
|
+
if (this.is_refreshing || this.is_loading) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (this.sort_column === column) {
|
|
302
|
+
this.sort_direction = this.sort_direction === 'asc' ? 'desc' : 'asc';
|
|
303
|
+
} else {
|
|
304
|
+
this.sort_column = column;
|
|
305
|
+
this.sort_direction = 'asc';
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
await this.refresh();
|
|
309
|
+
},
|
|
310
|
+
}"
|
|
311
|
+
@filter-applied.window="refresh()"
|
|
312
|
+
@item-mounted.window="handle_item_mounted($event)"
|
|
313
|
+
@item-cleared.window="handle_item_cleared()"
|
|
314
|
+
@total-count-updated.window="handle_total_count_updated($event)"
|
|
315
|
+
{% block scroll_event %}{% endblock %}
|
|
316
|
+
>
|
|
317
|
+
<script type="application/json" x-ref="shared_payload">{% if shared_payload %}{{ shared_payload|safe }}{% else %}{}{% endif %}</script>
|
|
318
|
+
|
|
319
|
+
{% block scroll_toolbar %}{% endblock %}
|
|
320
|
+
|
|
321
|
+
{% block scroll_container %}
|
|
322
|
+
<div
|
|
323
|
+
class="position-relative {% block scroll_container_class %}{% endblock %}"
|
|
324
|
+
style="height: {{ container_height|default:'600px' }}; overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch;"
|
|
325
|
+
x-ref="scroll_container"
|
|
326
|
+
>
|
|
327
|
+
{% block scroll_content_wrapper %}
|
|
328
|
+
<div x-ref="content_container">
|
|
329
|
+
{% block scroll_content %}{% endblock %}
|
|
330
|
+
</div>
|
|
331
|
+
{% endblock %}
|
|
332
|
+
|
|
333
|
+
{% block scroll_loading %}
|
|
334
|
+
<div x-cloak x-show="show_loading && !is_refreshing" class="py-3 text-center">
|
|
335
|
+
<div class="spinner-border text-app-primary" role="status"></div>
|
|
336
|
+
</div>
|
|
337
|
+
{% endblock %}
|
|
338
|
+
|
|
339
|
+
{% block scroll_trigger %}
|
|
340
|
+
<div style="height: 10px;" x-ref="infinite_scroll_trigger"></div>
|
|
341
|
+
{% endblock %}
|
|
342
|
+
</div>
|
|
343
|
+
{% endblock %}
|
|
344
|
+
|
|
345
|
+
{% block scroll_footer %}
|
|
346
|
+
{% include 'django_spire/infinite_scroll/element/footer.html' %}
|
|
347
|
+
{% endblock %}
|
|
348
|
+
</div>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<div class="row {{ footer_class|default:'mt-3' }}">
|
|
2
|
+
<div class="col text-start">
|
|
3
|
+
<span class="fs-7 text-app-secondary">
|
|
4
|
+
Showing <span x-text="loaded_count"></span> of <span x-text="total_count"></span> {{ footer_label|default:'items' }}
|
|
5
|
+
</span>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<div class="col-auto text-end">
|
|
9
|
+
{% block footer_action %}{% endblock %}
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|