From 6464f9f48bb91dd9a509d2a2a45faad57fcd699f Mon Sep 17 00:00:00 2001 From: Michael Collins <15347726+michaeljcollinsuk@users.noreply.github.com> Date: Thu, 13 Oct 2022 10:22:38 +0100 Subject: [PATCH] Feature/expanded nodes are stored in the page session so that the structure of the nav tree is retained after editing a menu (#153) * feat: store expanded/collapsed nodes in JS session object When editing a menu, the expanded/collapsed nodes are stored in a session object that lasts until the tab is closed. This allows the user to edit the menu, or delete a node, and when the menu reloads, they are displayed the menu with the same nodes still open. * feat: store expanded/collapsed for multiple menus Updates the JS so that the ID of the menu is used when storing the expanded/collapsed node ID's. This change means storing an array of IDs for each menu as a json string. By storing session data unique for the menu object it stops the session data being used for the wrong menu object, and allows multiple menu states to be stored in a session. * fix: update changelog Co-authored-by: Michael Collins --- CHANGELOG.rst | 2 + .../js/navigation-tree-admin.js | 46 +++++++++++++++++-- .../admin/tree_change_list_results.html | 2 +- .../templatetags/navigation_admin_tree.py | 4 +- tests/test_admin.py | 16 +++++++ 5 files changed, 64 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e2fafa578..f8e17e004 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,8 @@ Changelog Unreleased ========== +* feat: store a list of expanded nodes in a page session object so after editing a menu the +previous state is displayed 1.7.0 (2022-09-21) ================== diff --git a/djangocms_navigation/static/djangocms_navigation/js/navigation-tree-admin.js b/djangocms_navigation/static/djangocms_navigation/js/navigation-tree-admin.js index f640e8154..f8513ce95 100644 --- a/djangocms_navigation/static/djangocms_navigation/js/navigation-tree-admin.js +++ b/djangocms_navigation/static/djangocms_navigation/js/navigation-tree-admin.js @@ -12,6 +12,8 @@ Original code found in treebeard-admin.js DRAG_LINE_COLOR = '#AA00AA'; RECENTLY_FADE_DURATION = 2000; + const EXPANDED_SESSION_KEY = 'expanded-'; + // This is the basic Node class, which handles UI tree operations for each 'row' var Node = function (elem) { var $elem = $(elem); @@ -19,12 +21,14 @@ Original code found in treebeard-admin.js var parent_id = $elem.attr('parent'); var level = parseInt($elem.attr('level')); var children_num = parseInt($elem.attr('children-num')); + var menu_content_id = $("#result_list").data("menuContentId"); return { elem: elem, $elem: $elem, node_id: node_id, parent_id: parent_id, level: level, + expanded_key: EXPANDED_SESSION_KEY + menu_content_id, has_children: function () { return children_num > 0; }, @@ -63,7 +67,7 @@ Original code found in treebeard-admin.js } }).show(); }, - // collapse_all() and expand_all() show/hide the node + child nodes AND modifies classes: + // collapse_all() and expand_all() show/hide the node + child nodes AND modifies classes: // (In practice these functions are only used with the root node) collapse_all: function () { this.$elem.find('a.collapse').removeClass('expanded').addClass('collapsed'); @@ -71,6 +75,8 @@ Original code found in treebeard-admin.js let node = new Node(this); node.collapse_all(); }).hide(); + // clear storage so that on reload go back to default view + sessionStorage.clear() }, expand_all: function () { this.$elem.find('a.collapse').removeClass('collapsed').addClass('expanded'); @@ -78,6 +84,8 @@ Original code found in treebeard-admin.js let node = new Node(this); node.expand_all(); }).show(); + // clear storage so that on reload go back to default view + sessionStorage.clear() }, // Toggle show/hides the node (and child nodes), but does not modify child classes - this is so the 'state' can be perserved. toggle: function () { @@ -85,19 +93,51 @@ Original code found in treebeard-admin.js this.expand(); // Update classes just for this node: this.$elem.find('a.collapse').removeClass('collapsed').addClass('expanded'); + this.add_to_session() } else { this.collapse(); this.$elem.find('a.collapse').removeClass('expanded').addClass('collapsed'); + this.remove_from_session() } }, clone: function () { return $elem.clone(); + }, + add_to_session: function () { + // get or create an array of element ids that are expanded + let expanded = JSON.parse(sessionStorage.getItem(this.expanded_key)) || [] + expanded.push(this.elem.id) + sessionStorage.setItem(this.expanded_key, JSON.stringify(expanded)) + }, + remove_from_session: function () { + let expanded = JSON.parse(sessionStorage.getItem(this.expanded_key)) || [] + // filter the array to remove this element id + expanded = expanded.filter(elementId => elementId !== this.elem.id) + // also remove any child elements + if (this.has_children()) { + $.each(this.children(), function () { + expanded = expanded.filter(elementId => elementId !== this.id) + }) + } + // update the session + sessionStorage.setItem(this.expanded_key, JSON.stringify(expanded)) } } }; $(document).ready(function () { + // check the session for a stored state of expanded nodes + const menuContentId = $("#result_list").data("menuContentId"); + let expanded = JSON.parse(sessionStorage.getItem(EXPANDED_SESSION_KEY + menuContentId)) + if (expanded) { + expanded.forEach(function (elementId) { + var node = new Node($.find('#' + elementId)[0]) + node.expand() + node.$elem.find('a.collapse').removeClass('collapsed').addClass('expanded'); + }) + } + // begin csrf token code // Taken from http://docs.djangoproject.com/en/dev/ref/contrib/csrf/#ajax $(document).ajaxSend(function (event, xhr, settings) { @@ -344,7 +384,7 @@ Original code found in treebeard-admin.js // Get root node: let root_node = new Node($.find('tr[level=1]')[0]); - // Toggle expand / collapse all: + // Toggle expand / collapse all: if (!this.hasAttribute('class') || $(this).hasClass('collapsed-all') ) { root_node.expand_all(); $(this).addClass('expanded-all').removeClass('collapsed-all').text('-'); @@ -353,7 +393,7 @@ Original code found in treebeard-admin.js $(this).addClass('collapsed-all').removeClass('expanded-all').text('+'); } }); - + var hash = window.location.hash; if (hash) { diff --git a/djangocms_navigation/templates/djangocms_navigation/admin/tree_change_list_results.html b/djangocms_navigation/templates/djangocms_navigation/admin/tree_change_list_results.html index 1119230c0..f2e1a6abe 100644 --- a/djangocms_navigation/templates/djangocms_navigation/admin/tree_change_list_results.html +++ b/djangocms_navigation/templates/djangocms_navigation/admin/tree_change_list_results.html @@ -9,7 +9,7 @@ {% endif %} {% if results %}
- +
{% for header in result_headers %} diff --git a/djangocms_navigation/templatetags/navigation_admin_tree.py b/djangocms_navigation/templatetags/navigation_admin_tree.py index 011136c81..8b48b3f16 100644 --- a/djangocms_navigation/templatetags/navigation_admin_tree.py +++ b/djangocms_navigation/templatetags/navigation_admin_tree.py @@ -82,8 +82,8 @@ def result_tree(context, cl, request): 'result_headers': headers, 'results': list(results(cl)), 'disable_drag_drop': disable_drag_drop, - 'move_node_message': move_node_message - + 'move_node_message': move_node_message, + 'menu_content_id': context["menu_content"].pk } diff --git a/tests/test_admin.py b/tests/test_admin.py index e1bcd9aee..f1b87c131 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -1198,6 +1198,22 @@ def test_menuitem_move_message(self): element = soup.find("tbody") self.assertEqual(element.attrs["data-move-message"], "Are you sure you want to move menu item") + def test_menu_content_id_present(self): + """ + Check that the rendered template includes the menu content id as a data attribute so that it can be accessed by + the client side js + """ + menu_content = factories.MenuContentWithVersionFactory() + list_url = reverse( + "admin:djangocms_navigation_menuitem_list", args=(menu_content.id,) + ) + response = self.client.get(list_url) + + soup = BeautifulSoup(str(response.content), features="lxml") + result_list = soup.find(id="result_list") + + self.assertEqual(result_list["data-menu-content-id"], str(menu_content.pk)) + @override_settings( CMS_PERMISSION=True,