django-advanced-menus 1.0.0__tar.gz
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_advanced_menus-1.0.0/LICENSE +21 -0
- django_advanced_menus-1.0.0/MANIFEST.in +2 -0
- django_advanced_menus-1.0.0/PKG-INFO +228 -0
- django_advanced_menus-1.0.0/README.md +201 -0
- django_advanced_menus-1.0.0/django_advanced_menus.egg-info/PKG-INFO +228 -0
- django_advanced_menus-1.0.0/django_advanced_menus.egg-info/SOURCES.txt +53 -0
- django_advanced_menus-1.0.0/django_advanced_menus.egg-info/dependency_links.txt +1 -0
- django_advanced_menus-1.0.0/django_advanced_menus.egg-info/requires.txt +1 -0
- django_advanced_menus-1.0.0/django_advanced_menus.egg-info/top_level.txt +1 -0
- django_advanced_menus-1.0.0/django_menus/__init__.py +2 -0
- django_advanced_menus-1.0.0/django_menus/apps.py +5 -0
- django_advanced_menus-1.0.0/django_menus/includes.py +53 -0
- django_advanced_menus-1.0.0/django_menus/menu/__init__.py +4 -0
- django_advanced_menus-1.0.0/django_menus/menu/context_menu.py +14 -0
- django_advanced_menus-1.0.0/django_menus/menu/menu.py +201 -0
- django_advanced_menus-1.0.0/django_menus/menu/menu_items.py +474 -0
- django_advanced_menus-1.0.0/django_menus/menu/tabs.py +84 -0
- django_advanced_menus-1.0.0/django_menus/packs.py +59 -0
- django_advanced_menus-1.0.0/django_menus/static/django_menus/django_menus.css +14 -0
- django_advanced_menus-1.0.0/django_menus/static/django_menus/django_menus.js +445 -0
- django_advanced_menus-1.0.0/django_menus/static/django_menus/popper/popper.min.js +6 -0
- django_advanced_menus-1.0.0/django_menus/templates/django_menus/bootstrap4/ajax_dropdown.html +8 -0
- django_advanced_menus-1.0.0/django_menus/templates/django_menus/bootstrap4/ajax_tooltip.html +7 -0
- django_advanced_menus-1.0.0/django_menus/templates/django_menus/bootstrap4/badge.html +1 -0
- django_advanced_menus-1.0.0/django_menus/templates/django_menus/bootstrap4/breadcrumb.html +20 -0
- django_advanced_menus-1.0.0/django_menus/templates/django_menus/bootstrap4/button_group.html +13 -0
- django_advanced_menus-1.0.0/django_menus/templates/django_menus/bootstrap4/button_menu.html +13 -0
- django_advanced_menus-1.0.0/django_menus/templates/django_menus/bootstrap4/context_menu.html +14 -0
- django_advanced_menus-1.0.0/django_menus/templates/django_menus/bootstrap4/divider.html +1 -0
- django_advanced_menus-1.0.0/django_menus/templates/django_menus/bootstrap4/dropdown.html +20 -0
- django_advanced_menus-1.0.0/django_menus/templates/django_menus/bootstrap4/header.html +1 -0
- django_advanced_menus-1.0.0/django_menus/templates/django_menus/bootstrap4/main_menu.html +15 -0
- django_advanced_menus-1.0.0/django_menus/templates/django_menus/bootstrap4/single_button.html +3 -0
- django_advanced_menus-1.0.0/django_menus/templates/django_menus/bootstrap4/tab_menu.html +13 -0
- django_advanced_menus-1.0.0/django_menus/templates/django_menus/bootstrap5/ajax_dropdown.html +8 -0
- django_advanced_menus-1.0.0/django_menus/templates/django_menus/bootstrap5/ajax_tooltip.html +7 -0
- django_advanced_menus-1.0.0/django_menus/templates/django_menus/bootstrap5/badge.html +1 -0
- django_advanced_menus-1.0.0/django_menus/templates/django_menus/bootstrap5/breadcrumb.html +20 -0
- django_advanced_menus-1.0.0/django_menus/templates/django_menus/bootstrap5/button_group.html +13 -0
- django_advanced_menus-1.0.0/django_menus/templates/django_menus/bootstrap5/button_menu.html +13 -0
- django_advanced_menus-1.0.0/django_menus/templates/django_menus/bootstrap5/context_menu.html +14 -0
- django_advanced_menus-1.0.0/django_menus/templates/django_menus/bootstrap5/divider.html +1 -0
- django_advanced_menus-1.0.0/django_menus/templates/django_menus/bootstrap5/dropdown.html +20 -0
- django_advanced_menus-1.0.0/django_menus/templates/django_menus/bootstrap5/header.html +1 -0
- django_advanced_menus-1.0.0/django_menus/templates/django_menus/bootstrap5/main_menu.html +15 -0
- django_advanced_menus-1.0.0/django_menus/templates/django_menus/bootstrap5/single_button.html +3 -0
- django_advanced_menus-1.0.0/django_menus/templates/django_menus/bootstrap5/tab_menu.html +13 -0
- django_advanced_menus-1.0.0/django_menus/templates/django_menus/menu_key_press.html +23 -0
- django_advanced_menus-1.0.0/django_menus/templates/django_menus/script.html +7 -0
- django_advanced_menus-1.0.0/django_menus/templatetags/__init__.py +0 -0
- django_advanced_menus-1.0.0/django_menus/templatetags/django_menu_tags.py +34 -0
- django_advanced_menus-1.0.0/django_menus/test_settings.py +34 -0
- django_advanced_menus-1.0.0/django_menus/tests.py +88 -0
- django_advanced_menus-1.0.0/setup.cfg +4 -0
- django_advanced_menus-1.0.0/setup.py +25 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021 Ian Jones
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-advanced-menus
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Django app to render menus and load tabs with Ajax
|
|
5
|
+
Home-page: https://github.com/django-advance-utils/django-advanced-menus
|
|
6
|
+
Author: Ian Jones
|
|
7
|
+
Maintainer: Thomas Turner
|
|
8
|
+
Project-URL: Upstream, https://github.com/jonesim/django-menus
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.6
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: ajax-advanced-helpers>=1.0.1
|
|
16
|
+
Dynamic: author
|
|
17
|
+
Dynamic: classifier
|
|
18
|
+
Dynamic: description
|
|
19
|
+
Dynamic: description-content-type
|
|
20
|
+
Dynamic: home-page
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
Dynamic: maintainer
|
|
23
|
+
Dynamic: project-url
|
|
24
|
+
Dynamic: requires-dist
|
|
25
|
+
Dynamic: requires-python
|
|
26
|
+
Dynamic: summary
|
|
27
|
+
|
|
28
|
+
[](https://badge.fury.io/py/django-advanced-menus)
|
|
29
|
+
|
|
30
|
+
# django-advanced-menus
|
|
31
|
+
|
|
32
|
+
Django app to render menus and load tabs with Ajax.
|
|
33
|
+
|
|
34
|
+
The [django-advance-utils](https://github.com/django-advance-utils) line of Ian Jones's
|
|
35
|
+
[django-tab-menus](https://github.com/jonesim/django-menus), forked so that releases can be cut as
|
|
36
|
+
the downstream libraries and django-advanced-report-builder need them. The Python package is still
|
|
37
|
+
`django_menus`, so existing imports and `INSTALLED_APPS` entries do not change; only the pip name
|
|
38
|
+
does. It depends on [ajax-advanced-helpers](https://github.com/django-advance-utils/ajax-advanced-helpers).
|
|
39
|
+
|
|
40
|
+
pip install django-advanced-menus
|
|
41
|
+
|
|
42
|
+
See example django project with docker compose file
|
|
43
|
+
|
|
44
|
+
Add to installed apps in settings
|
|
45
|
+
`'django_menus',`
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
### Repeat clicks on menu links
|
|
49
|
+
|
|
50
|
+
Every menu item renders as an `<a href>`, and a menu item can point at a view that *does*
|
|
51
|
+
something rather than one that just shows a page. A double-click sends the URL twice, and a view
|
|
52
|
+
that resolves what to act on from its own current state - rather than from the state the link was
|
|
53
|
+
drawn against - acts on both.
|
|
54
|
+
|
|
55
|
+
This is **off by default** - swallowing a click is a behaviour change, and whether a project has
|
|
56
|
+
menu items pointing at views that mind being called twice is the project's business. Opt in with
|
|
57
|
+
the milliseconds to hold a link for, at whichever level fits. They cascade
|
|
58
|
+
**item -> menu -> page -> off**, so the narrowest one set wins:
|
|
59
|
+
|
|
60
|
+
# one item - usually the one that knows it minds being clicked twice
|
|
61
|
+
MenuItem('next_stage', 'Go to Next Stage', django_menus_repeat_click_ms=2000)
|
|
62
|
+
|
|
63
|
+
# every item in a menu that has not set its own
|
|
64
|
+
HtmlMenu(request, 'button_group', django_menus_repeat_click_ms=2000)
|
|
65
|
+
|
|
66
|
+
# a page, or a whole site if set on a base view class
|
|
67
|
+
class MyView(MenuTemplateView):
|
|
68
|
+
repeat_click_ms = 2000
|
|
69
|
+
|
|
70
|
+
A target view can also declare it, which is arguably the best place - the view that minds being
|
|
71
|
+
called twice is the thing that knows:
|
|
72
|
+
|
|
73
|
+
class MyView(View):
|
|
74
|
+
menu_config = {'attributes': {'data-django-menus-repeat-ms': 2000}}
|
|
75
|
+
|
|
76
|
+
That sits below the item and menu arguments, which are set at the call site and so win.
|
|
77
|
+
|
|
78
|
+
Because an item's own value wins, one item can be held on a page with the guard off, and
|
|
79
|
+
`django_menus_repeat_click_ms=0` on an item opts it out where the page has it on. A menu-level
|
|
80
|
+
value also reaches the items of a dropdown built on one of that menu's items.
|
|
81
|
+
|
|
82
|
+
The first two forms render a `data-django-menus-repeat-ms` attribute on the anchor and need
|
|
83
|
+
nothing else. The view attribute reaches the page through the `django_menus_script` context
|
|
84
|
+
variable, so output that once in a base template, alongside the include:
|
|
85
|
+
|
|
86
|
+
{% lib_include module='django_menus.includes' %}
|
|
87
|
+
{{ django_menus_script }}
|
|
88
|
+
|
|
89
|
+
All that does is set a JS window of the same name, which the guard reads on every click rather
|
|
90
|
+
than capturing at load - so it can also be set or changed directly, from a page's own script or a
|
|
91
|
+
console while diagnosing, on either side of the include:
|
|
92
|
+
|
|
93
|
+
django_menus_repeat_click_ms = 2000;
|
|
94
|
+
|
|
95
|
+
Anywhere it is set, `0` turns it off, and anything that is not a number above zero reads as off.
|
|
96
|
+
|
|
97
|
+
See the **Repeat Clicks** page in the example app for all three working side by side.
|
|
98
|
+
|
|
99
|
+
Once on, a second click on the same anchor inside that window is swallowed. Only real navigations
|
|
100
|
+
are affected - a `javascript:` href leaves the page in place and clicking again straight away is
|
|
101
|
+
often what the user means (close a modal, reopen it), so those are left alone, as are modified
|
|
102
|
+
clicks (ctrl/cmd/shift/alt, or any button but the primary) which open the link elsewhere.
|
|
103
|
+
|
|
104
|
+
Two limits worth knowing. The window collapses a double-click; it does not cover an impatient
|
|
105
|
+
re-click several seconds into a slow response, because holding a link until the page actually
|
|
106
|
+
goes away would strand anything that deliberately leaves the page up, like a download or a
|
|
107
|
+
`target="_blank"`. And the rule is about the href, not the effect: a `JAVASCRIPT` item is free to
|
|
108
|
+
set `window.location` itself, an `AJAX_BUTTON`'s view can answer with a redirect, and
|
|
109
|
+
`AJAX_GET_URL_NAME` fetches a view - those hit the view twice on a double-click just the same and
|
|
110
|
+
are out of scope here.
|
|
111
|
+
|
|
112
|
+
If you override these templates with your own copies, carry the `django-menus-item` class across
|
|
113
|
+
or those menus will not be covered.
|
|
114
|
+
|
|
115
|
+
### Showing that the click registered
|
|
116
|
+
|
|
117
|
+
While a link is held it carries a `django-menus-clicked` class. Swallowing the second click stops
|
|
118
|
+
the duplicate request, but people double-click *because* the first click appeared to do nothing,
|
|
119
|
+
so the class is there to let you address the cause as well:
|
|
120
|
+
|
|
121
|
+
a.django-menus-clicked { opacity: .65; cursor: default; }
|
|
122
|
+
|
|
123
|
+
No styling is shipped for it, so it does nothing until a project adds a rule. Style the
|
|
124
|
+
appearance only - `pointer-events: none` looks like the obvious choice and lets the click fall
|
|
125
|
+
*through* to whatever sits underneath, which inside a dropdown is another menu item. The click is
|
|
126
|
+
already stopped in JS; the class only has to look the part.
|
|
127
|
+
|
|
128
|
+
The class is cleared when the hold expires, so a navigation that never arrives - cancelled, a
|
|
129
|
+
download, a `target="_blank"` - cannot leave a link looking permanently dead.
|
|
130
|
+
|
|
131
|
+
This is defence in depth, not a substitute for making such a view idempotent - the back button, a
|
|
132
|
+
refresh and a second tab all still send the request twice.
|
|
133
|
+
|
|
134
|
+
## Bootstrap 4 and Bootstrap 5
|
|
135
|
+
|
|
136
|
+
Menus ships a template pack per Bootstrap version:
|
|
137
|
+
|
|
138
|
+
django_menus/templates/django_menus/bootstrap4/
|
|
139
|
+
django_menus/templates/django_menus/bootstrap5/
|
|
140
|
+
|
|
141
|
+
Pick one in settings. Bootstrap 4 is the default:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
DJANGO_MENUS_TEMPLATE_PACK = 'bootstrap5'
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Each pack holds the complete set of menu templates and emits only its own version's names, so
|
|
148
|
+
the rendered page carries no classes the browser will ignore, and the two packs are free to
|
|
149
|
+
diverge structurally where Bootstrap 5 changed more than a name.
|
|
150
|
+
|
|
151
|
+
**Serving both from one deployment.** The setting may instead be a dotted path to a callable
|
|
152
|
+
taking the request and returning a pack name — a pack name never contains a dot, which is what
|
|
153
|
+
tells the two apart. The example app uses that for its nav bar toggle:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
DJANGO_MENUS_TEMPLATE_PACK = 'menu_examples.context_processors.template_pack_for_request'
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Overriding a template.** Menus no longer ships anything at the old flat paths, and the flat
|
|
160
|
+
path is tried *before* the pack. So a project that already overrides `django_menus/main_menu.html`
|
|
161
|
+
in its own templates directory keeps that override, and anything it does not override falls
|
|
162
|
+
through to the pack. One consequence worth knowing: an override is version-agnostic by
|
|
163
|
+
definition, so it applies to both packs.
|
|
164
|
+
|
|
165
|
+
**What a pack does not reach.** Two things, both deliberate:
|
|
166
|
+
|
|
167
|
+
- A tooltip's placement is a key in the attributes dict rather than markup, so its Bootstrap 5
|
|
168
|
+
rename lives in `django_menus/packs.py` as `PACK_ATTRIBUTES`. It is the only such entry.
|
|
169
|
+
- `css_classes`, `MenuItemDisplay`, the badge `css_class` and `attributes` are passed through
|
|
170
|
+
untouched, so those are yours to spell:
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
MenuItem('view1', 'Edit', css_classes=['btn-primary', 'me-1'])
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`menu_key_press.html` and `script.html` stay outside the packs — one is a keyboard handler and the
|
|
177
|
+
other sets the repeat-click window, neither is Bootstrap markup.
|
|
178
|
+
|
|
179
|
+
**Running the examples on either version.** The nav bar carries a `BS4 → BS5` toggle; it puts
|
|
180
|
+
`?bootstrap=5` on the URL and remembers the choice in the session, so a menu can be compared on
|
|
181
|
+
both without a restart. `MENUS_EXAMPLE_BOOTSTRAP=5` in the environment, or in settings, sets
|
|
182
|
+
where a fresh session starts.
|
|
183
|
+
|
|
184
|
+
Bootstrap 4 stays the default because the rest of the stack still emits it. On the Bootstrap 5
|
|
185
|
+
page the menus are correct and django-modals, show_src_code and crispy's template pack are
|
|
186
|
+
not — the example app makes that visible rather than hiding it, and it is the remaining work.
|
|
187
|
+
|
|
188
|
+
**Two settings, two paragraphs.** The template pack above is chosen by `DJANGO_MENUS_TEMPLATE_PACK`;
|
|
189
|
+
the scripts a page loads are chosen by the ecosystem-wide `CSS_FRAMEWORK` that ajax-helpers reads
|
|
190
|
+
(see the positioning section below). This paragraph describes Bootstrap 5 markup on a page whose
|
|
191
|
+
`CSS_FRAMEWORK` is still `bootstrap4`, so ajax-helpers still loads jQuery and Popper 1. With both
|
|
192
|
+
set to `bootstrap5`, ajax-helpers 1.0.1 loads neither and the menus include loads Popper 2 itself.
|
|
193
|
+
|
|
194
|
+
**The one load-order requirement.** jQuery must load before Bootstrap 5, which is what
|
|
195
|
+
`base.html` does. Bootstrap 5 dropped jQuery as a dependency and registers its plugin interface
|
|
196
|
+
only when jQuery got there first; without it `ajax_helpers.tooltip` quietly does nothing. The
|
|
197
|
+
same order keeps `window.Popper` pointing at the Popper 1 that ajax_helpers ships, which is what
|
|
198
|
+
the dropdown positioning is written against — Bootstrap 5's bundle keeps its own Popper 2
|
|
199
|
+
private. `django_menus.js` detects Popper 2 anyway, for a project that brings its own.
|
|
200
|
+
|
|
201
|
+
**One template this does not reach.** `ajax_tooltip.html` passes a Bootstrap-classed template to
|
|
202
|
+
`ajax_helpers.tooltip`, and ajax_helpers 1.0.0 rewrote that function around its own `ah-`
|
|
203
|
+
classes — it reads `.ah-tooltip-inner` out of whatever template it is handed. That template no
|
|
204
|
+
longer matches on either Bootstrap version, independently of anything here. No example view
|
|
205
|
+
exercises it. The fix is to stop passing a Bootstrap template and let ajax_helpers use its own,
|
|
206
|
+
which is version-neutral; it is left alone here because it is not a Bootstrap 5 question.
|
|
207
|
+
|
|
208
|
+
**Keeping the packs in step.** Two folders means a fix can land in one and not the other, and
|
|
209
|
+
unlike a missing class name that is invisible on whichever version you are not looking at. So
|
|
210
|
+
`menu_examples/tests/test_template_packs.py` normalises every Bootstrap 4 name in a pack4
|
|
211
|
+
template to its Bootstrap 5 spelling and requires the result to equal the pack5 file exactly.
|
|
212
|
+
A deliberate divergence goes in `STRUCTURAL_DIVERGENCE` with a note, which is the point at
|
|
213
|
+
which someone has to think about it. It is empty today: every difference is still a rename.
|
|
214
|
+
|
|
215
|
+
## Bootstrap 4 and Bootstrap 5: positioning the dropdowns
|
|
216
|
+
|
|
217
|
+
Dropdown menus are positioned with Popper. Bootstrap 4 puts Popper 1 on the page as a global
|
|
218
|
+
constructor, and that is what the menus have always used. Bootstrap 5 bundles Popper 2 privately
|
|
219
|
+
and leaves `window.Popper` unset, so under Bootstrap 5 the default include loads `@popperjs/core`
|
|
220
|
+
itself (vendored, with a jsDelivr fallback) and the script detects whichever generation it finds.
|
|
221
|
+
The switch is the ecosystem-wide `CSS_FRAMEWORK` setting that ajax-helpers reads:
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
CSS_FRAMEWORK = 'bootstrap5' # default 'bootstrap4'
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Nothing changes under Bootstrap 4. With no Popper on the page at all, a menu opens straight
|
|
228
|
+
below its button.
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
[](https://badge.fury.io/py/django-advanced-menus)
|
|
2
|
+
|
|
3
|
+
# django-advanced-menus
|
|
4
|
+
|
|
5
|
+
Django app to render menus and load tabs with Ajax.
|
|
6
|
+
|
|
7
|
+
The [django-advance-utils](https://github.com/django-advance-utils) line of Ian Jones's
|
|
8
|
+
[django-tab-menus](https://github.com/jonesim/django-menus), forked so that releases can be cut as
|
|
9
|
+
the downstream libraries and django-advanced-report-builder need them. The Python package is still
|
|
10
|
+
`django_menus`, so existing imports and `INSTALLED_APPS` entries do not change; only the pip name
|
|
11
|
+
does. It depends on [ajax-advanced-helpers](https://github.com/django-advance-utils/ajax-advanced-helpers).
|
|
12
|
+
|
|
13
|
+
pip install django-advanced-menus
|
|
14
|
+
|
|
15
|
+
See example django project with docker compose file
|
|
16
|
+
|
|
17
|
+
Add to installed apps in settings
|
|
18
|
+
`'django_menus',`
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Repeat clicks on menu links
|
|
22
|
+
|
|
23
|
+
Every menu item renders as an `<a href>`, and a menu item can point at a view that *does*
|
|
24
|
+
something rather than one that just shows a page. A double-click sends the URL twice, and a view
|
|
25
|
+
that resolves what to act on from its own current state - rather than from the state the link was
|
|
26
|
+
drawn against - acts on both.
|
|
27
|
+
|
|
28
|
+
This is **off by default** - swallowing a click is a behaviour change, and whether a project has
|
|
29
|
+
menu items pointing at views that mind being called twice is the project's business. Opt in with
|
|
30
|
+
the milliseconds to hold a link for, at whichever level fits. They cascade
|
|
31
|
+
**item -> menu -> page -> off**, so the narrowest one set wins:
|
|
32
|
+
|
|
33
|
+
# one item - usually the one that knows it minds being clicked twice
|
|
34
|
+
MenuItem('next_stage', 'Go to Next Stage', django_menus_repeat_click_ms=2000)
|
|
35
|
+
|
|
36
|
+
# every item in a menu that has not set its own
|
|
37
|
+
HtmlMenu(request, 'button_group', django_menus_repeat_click_ms=2000)
|
|
38
|
+
|
|
39
|
+
# a page, or a whole site if set on a base view class
|
|
40
|
+
class MyView(MenuTemplateView):
|
|
41
|
+
repeat_click_ms = 2000
|
|
42
|
+
|
|
43
|
+
A target view can also declare it, which is arguably the best place - the view that minds being
|
|
44
|
+
called twice is the thing that knows:
|
|
45
|
+
|
|
46
|
+
class MyView(View):
|
|
47
|
+
menu_config = {'attributes': {'data-django-menus-repeat-ms': 2000}}
|
|
48
|
+
|
|
49
|
+
That sits below the item and menu arguments, which are set at the call site and so win.
|
|
50
|
+
|
|
51
|
+
Because an item's own value wins, one item can be held on a page with the guard off, and
|
|
52
|
+
`django_menus_repeat_click_ms=0` on an item opts it out where the page has it on. A menu-level
|
|
53
|
+
value also reaches the items of a dropdown built on one of that menu's items.
|
|
54
|
+
|
|
55
|
+
The first two forms render a `data-django-menus-repeat-ms` attribute on the anchor and need
|
|
56
|
+
nothing else. The view attribute reaches the page through the `django_menus_script` context
|
|
57
|
+
variable, so output that once in a base template, alongside the include:
|
|
58
|
+
|
|
59
|
+
{% lib_include module='django_menus.includes' %}
|
|
60
|
+
{{ django_menus_script }}
|
|
61
|
+
|
|
62
|
+
All that does is set a JS window of the same name, which the guard reads on every click rather
|
|
63
|
+
than capturing at load - so it can also be set or changed directly, from a page's own script or a
|
|
64
|
+
console while diagnosing, on either side of the include:
|
|
65
|
+
|
|
66
|
+
django_menus_repeat_click_ms = 2000;
|
|
67
|
+
|
|
68
|
+
Anywhere it is set, `0` turns it off, and anything that is not a number above zero reads as off.
|
|
69
|
+
|
|
70
|
+
See the **Repeat Clicks** page in the example app for all three working side by side.
|
|
71
|
+
|
|
72
|
+
Once on, a second click on the same anchor inside that window is swallowed. Only real navigations
|
|
73
|
+
are affected - a `javascript:` href leaves the page in place and clicking again straight away is
|
|
74
|
+
often what the user means (close a modal, reopen it), so those are left alone, as are modified
|
|
75
|
+
clicks (ctrl/cmd/shift/alt, or any button but the primary) which open the link elsewhere.
|
|
76
|
+
|
|
77
|
+
Two limits worth knowing. The window collapses a double-click; it does not cover an impatient
|
|
78
|
+
re-click several seconds into a slow response, because holding a link until the page actually
|
|
79
|
+
goes away would strand anything that deliberately leaves the page up, like a download or a
|
|
80
|
+
`target="_blank"`. And the rule is about the href, not the effect: a `JAVASCRIPT` item is free to
|
|
81
|
+
set `window.location` itself, an `AJAX_BUTTON`'s view can answer with a redirect, and
|
|
82
|
+
`AJAX_GET_URL_NAME` fetches a view - those hit the view twice on a double-click just the same and
|
|
83
|
+
are out of scope here.
|
|
84
|
+
|
|
85
|
+
If you override these templates with your own copies, carry the `django-menus-item` class across
|
|
86
|
+
or those menus will not be covered.
|
|
87
|
+
|
|
88
|
+
### Showing that the click registered
|
|
89
|
+
|
|
90
|
+
While a link is held it carries a `django-menus-clicked` class. Swallowing the second click stops
|
|
91
|
+
the duplicate request, but people double-click *because* the first click appeared to do nothing,
|
|
92
|
+
so the class is there to let you address the cause as well:
|
|
93
|
+
|
|
94
|
+
a.django-menus-clicked { opacity: .65; cursor: default; }
|
|
95
|
+
|
|
96
|
+
No styling is shipped for it, so it does nothing until a project adds a rule. Style the
|
|
97
|
+
appearance only - `pointer-events: none` looks like the obvious choice and lets the click fall
|
|
98
|
+
*through* to whatever sits underneath, which inside a dropdown is another menu item. The click is
|
|
99
|
+
already stopped in JS; the class only has to look the part.
|
|
100
|
+
|
|
101
|
+
The class is cleared when the hold expires, so a navigation that never arrives - cancelled, a
|
|
102
|
+
download, a `target="_blank"` - cannot leave a link looking permanently dead.
|
|
103
|
+
|
|
104
|
+
This is defence in depth, not a substitute for making such a view idempotent - the back button, a
|
|
105
|
+
refresh and a second tab all still send the request twice.
|
|
106
|
+
|
|
107
|
+
## Bootstrap 4 and Bootstrap 5
|
|
108
|
+
|
|
109
|
+
Menus ships a template pack per Bootstrap version:
|
|
110
|
+
|
|
111
|
+
django_menus/templates/django_menus/bootstrap4/
|
|
112
|
+
django_menus/templates/django_menus/bootstrap5/
|
|
113
|
+
|
|
114
|
+
Pick one in settings. Bootstrap 4 is the default:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
DJANGO_MENUS_TEMPLATE_PACK = 'bootstrap5'
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Each pack holds the complete set of menu templates and emits only its own version's names, so
|
|
121
|
+
the rendered page carries no classes the browser will ignore, and the two packs are free to
|
|
122
|
+
diverge structurally where Bootstrap 5 changed more than a name.
|
|
123
|
+
|
|
124
|
+
**Serving both from one deployment.** The setting may instead be a dotted path to a callable
|
|
125
|
+
taking the request and returning a pack name — a pack name never contains a dot, which is what
|
|
126
|
+
tells the two apart. The example app uses that for its nav bar toggle:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
DJANGO_MENUS_TEMPLATE_PACK = 'menu_examples.context_processors.template_pack_for_request'
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Overriding a template.** Menus no longer ships anything at the old flat paths, and the flat
|
|
133
|
+
path is tried *before* the pack. So a project that already overrides `django_menus/main_menu.html`
|
|
134
|
+
in its own templates directory keeps that override, and anything it does not override falls
|
|
135
|
+
through to the pack. One consequence worth knowing: an override is version-agnostic by
|
|
136
|
+
definition, so it applies to both packs.
|
|
137
|
+
|
|
138
|
+
**What a pack does not reach.** Two things, both deliberate:
|
|
139
|
+
|
|
140
|
+
- A tooltip's placement is a key in the attributes dict rather than markup, so its Bootstrap 5
|
|
141
|
+
rename lives in `django_menus/packs.py` as `PACK_ATTRIBUTES`. It is the only such entry.
|
|
142
|
+
- `css_classes`, `MenuItemDisplay`, the badge `css_class` and `attributes` are passed through
|
|
143
|
+
untouched, so those are yours to spell:
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
MenuItem('view1', 'Edit', css_classes=['btn-primary', 'me-1'])
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`menu_key_press.html` and `script.html` stay outside the packs — one is a keyboard handler and the
|
|
150
|
+
other sets the repeat-click window, neither is Bootstrap markup.
|
|
151
|
+
|
|
152
|
+
**Running the examples on either version.** The nav bar carries a `BS4 → BS5` toggle; it puts
|
|
153
|
+
`?bootstrap=5` on the URL and remembers the choice in the session, so a menu can be compared on
|
|
154
|
+
both without a restart. `MENUS_EXAMPLE_BOOTSTRAP=5` in the environment, or in settings, sets
|
|
155
|
+
where a fresh session starts.
|
|
156
|
+
|
|
157
|
+
Bootstrap 4 stays the default because the rest of the stack still emits it. On the Bootstrap 5
|
|
158
|
+
page the menus are correct and django-modals, show_src_code and crispy's template pack are
|
|
159
|
+
not — the example app makes that visible rather than hiding it, and it is the remaining work.
|
|
160
|
+
|
|
161
|
+
**Two settings, two paragraphs.** The template pack above is chosen by `DJANGO_MENUS_TEMPLATE_PACK`;
|
|
162
|
+
the scripts a page loads are chosen by the ecosystem-wide `CSS_FRAMEWORK` that ajax-helpers reads
|
|
163
|
+
(see the positioning section below). This paragraph describes Bootstrap 5 markup on a page whose
|
|
164
|
+
`CSS_FRAMEWORK` is still `bootstrap4`, so ajax-helpers still loads jQuery and Popper 1. With both
|
|
165
|
+
set to `bootstrap5`, ajax-helpers 1.0.1 loads neither and the menus include loads Popper 2 itself.
|
|
166
|
+
|
|
167
|
+
**The one load-order requirement.** jQuery must load before Bootstrap 5, which is what
|
|
168
|
+
`base.html` does. Bootstrap 5 dropped jQuery as a dependency and registers its plugin interface
|
|
169
|
+
only when jQuery got there first; without it `ajax_helpers.tooltip` quietly does nothing. The
|
|
170
|
+
same order keeps `window.Popper` pointing at the Popper 1 that ajax_helpers ships, which is what
|
|
171
|
+
the dropdown positioning is written against — Bootstrap 5's bundle keeps its own Popper 2
|
|
172
|
+
private. `django_menus.js` detects Popper 2 anyway, for a project that brings its own.
|
|
173
|
+
|
|
174
|
+
**One template this does not reach.** `ajax_tooltip.html` passes a Bootstrap-classed template to
|
|
175
|
+
`ajax_helpers.tooltip`, and ajax_helpers 1.0.0 rewrote that function around its own `ah-`
|
|
176
|
+
classes — it reads `.ah-tooltip-inner` out of whatever template it is handed. That template no
|
|
177
|
+
longer matches on either Bootstrap version, independently of anything here. No example view
|
|
178
|
+
exercises it. The fix is to stop passing a Bootstrap template and let ajax_helpers use its own,
|
|
179
|
+
which is version-neutral; it is left alone here because it is not a Bootstrap 5 question.
|
|
180
|
+
|
|
181
|
+
**Keeping the packs in step.** Two folders means a fix can land in one and not the other, and
|
|
182
|
+
unlike a missing class name that is invisible on whichever version you are not looking at. So
|
|
183
|
+
`menu_examples/tests/test_template_packs.py` normalises every Bootstrap 4 name in a pack4
|
|
184
|
+
template to its Bootstrap 5 spelling and requires the result to equal the pack5 file exactly.
|
|
185
|
+
A deliberate divergence goes in `STRUCTURAL_DIVERGENCE` with a note, which is the point at
|
|
186
|
+
which someone has to think about it. It is empty today: every difference is still a rename.
|
|
187
|
+
|
|
188
|
+
## Bootstrap 4 and Bootstrap 5: positioning the dropdowns
|
|
189
|
+
|
|
190
|
+
Dropdown menus are positioned with Popper. Bootstrap 4 puts Popper 1 on the page as a global
|
|
191
|
+
constructor, and that is what the menus have always used. Bootstrap 5 bundles Popper 2 privately
|
|
192
|
+
and leaves `window.Popper` unset, so under Bootstrap 5 the default include loads `@popperjs/core`
|
|
193
|
+
itself (vendored, with a jsDelivr fallback) and the script detects whichever generation it finds.
|
|
194
|
+
The switch is the ecosystem-wide `CSS_FRAMEWORK` setting that ajax-helpers reads:
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
CSS_FRAMEWORK = 'bootstrap5' # default 'bootstrap4'
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Nothing changes under Bootstrap 4. With no Popper on the page at all, a menu opens straight
|
|
201
|
+
below its button.
|