Skip to content

Commit

Permalink
Use Sortable.js to sort Menus
Browse files Browse the repository at this point in the history
This commit allows sorting of menus. We instantiate for each element in
the tree one instance of Sortable.js, but - through the `group` option
- allow dragging and dropping between each element. For this to work,
  all `ul` elements need to be present (otherwise we cannot drop a node
below a list that does not have nodes yet).

A little tricky was the correct behaviour of the `folded` button: It
should only be displayed if there are any pages to fold. This is now
accomplished through rendering the `+` and `-` buttons in Javascript,
depending on how many elements are in each list after sorting.

Additionally: The full node tree is now rendered on page load, and all
manipulations manipulate the DOM. There are no nodes being inserted or
deleted through sorting or folding.
  • Loading branch information
mamhoff committed Mar 23, 2020
1 parent b6151b4 commit 074882e
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 42 deletions.
1 change: 1 addition & 0 deletions app/assets/javascripts/alchemy/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
//= require alchemy/alchemy.link_dialog
//= require alchemy/alchemy.list_filter
//= require alchemy/alchemy.initializer
//= require alchemy/alchemy.node_tree
//= require alchemy/alchemy.page_sorter
//= require alchemy/alchemy.uploader
//= require alchemy/alchemy.preview_window
Expand Down
93 changes: 93 additions & 0 deletions app/assets/javascripts/alchemy/alchemy.node_tree.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
Alchemy.NodeTree = {
onFinishDragging: function (evt) {
var url = '/api/nodes/' + evt.item.dataset.id + '/move.json'
var xhr = new XMLHttpRequest()
var token = document.querySelector('meta[name="csrf-token"]').attributes.content.textContent
var data = {
target_parent_id: evt.to.dataset.nodeId,
new_position: evt.newIndex
};
var json = JSON.stringify(data)
xhr.open("PATCH", url);
xhr.setRequestHeader('Content-type', 'application/json; charset=utf-8');
xhr.setRequestHeader('X-CSRF-Token', token)
evt.to.parentElement.dataset.folded = 'false'
evt.to.classList.remove('folded')
xhr.onload = function () {
response_json = JSON.parse(xhr.responseText)
if (xhr.readyState == 4 && xhr.status == "200") {
Alchemy.NodeTree.displayNodeFolders()
} else {
Alchemy.growl(response_json.error, 'error');
}
}
xhr.send(json)
},

displayNodeFolders: function () {
var generate_link = function (node_id, folded) {
var icon = folded === "true" ? 'plus' : 'minus';
return '<a class="node_folder" data-node-id="' + node_id + '"><i class="far fa-' + icon + '-square fa-fw"></i></a>'
}

document.querySelectorAll('li.menu-item').forEach(function (el) {
var leftIconArea = el.querySelector('.nodes_tree-left_images')
var list = el.querySelector('ul')

if (list.children.length > 0 || el.dataset.folded === 'true' ) {
leftIconArea.innerHTML = generate_link(el.dataset.id, el.dataset.folded)
} else {
leftIconArea.innerHTML = '&nbsp;'
}
});

this.handleNodeFolders();
},

handleNodeFolders: function() {
var folders = document.querySelectorAll('.nodes_tree .node_folder');

folders.forEach(function(folder){
folder.addEventListener('click', function(evt) {
var nodeId = this.dataset.nodeId
var menu_item = this.closest('li.menu-item')
var url = '/admin/nodes/' + nodeId + '/toggle.html'
var list = menu_item.querySelector('.children')
var xhr = new XMLHttpRequest()
var token = document.querySelector('meta[name="csrf-token"]').attributes.content.textContent

xhr.open("PATCH", url);
xhr.setRequestHeader('Content-type', 'application/json; charset=utf-8');
xhr.setRequestHeader('X-CSRF-Token', token)

xhr.onload = function () {
if (xhr.readyState == 4 && xhr.status == "200") {
//list.innerHTML = xhr.responseText
list.classList.toggle('folded')
menu_item.dataset.folded = menu_item.dataset.folded == 'true' ? 'false' : 'true'
Alchemy.NodeTree.displayNodeFolders();
} else {
Alchemy.growl('error folding');
}
}
xhr.send()
})
});
},

init: function() {
this.displayNodeFolders()

document.querySelectorAll('.nodes_tree ul.children').forEach(function (el) {
new Sortable(el, {
group: 'nodes',
animation: 150,
fallbackOnBody: true,
swapThreshold: 0.65,
handle: '.node_name',
invertSwap: true,
onEnd: Alchemy.NodeTree.onFinishDragging
});
});
}
}
3 changes: 3 additions & 0 deletions app/assets/stylesheets/alchemy/nodes.scss
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@
ul {
margin: 0;
padding: 0;
.folded > li {
display: none;
}
}

li {
Expand Down
6 changes: 1 addition & 5 deletions app/controllers/alchemy/admin/nodes_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,7 @@ def new
def toggle
node = Node.find(params[:id])
node.update(folded: !node.folded)
if node.folded?
head :ok
else
render partial: 'node', collection: node.children.includes(:page, :children)
end
head :ok
end

private
Expand Down
24 changes: 5 additions & 19 deletions app/views/alchemy/admin/nodes/_node.html.erb
Original file line number Diff line number Diff line change
@@ -1,21 +1,11 @@
<li>
<%= content_tag :li, class: 'menu-item', data: { id: node.id, parent_id: node.parent_id, folded: node.folded? } do %>
<%= content_tag :div, class: [
'sitemap_node',
node.external? ? 'external' : 'internal',
"sitemap_node-level_#{node.depth}"
] do %>
<span class="nodes_tree-left_images">
<% if node.children.any? %>
<a class="node_folder" data-node-id="<%= node.id %>">
<% if node.folded? %>
<i class="far fa-plus-square fa-fw"></i>
<% else %>
<i class="far fa-minus-square fa-fw"></i>
<% end %>
</a>
<% else %>
&nbsp;
<% end %>
&nbsp;
</span>
<span class="nodes_tree-right_tools">
<% if can?(:edit, node) %>
Expand Down Expand Up @@ -81,11 +71,7 @@
<% end %>
</div>
<% end %>
<% if node.children.any? %>
<ul class="children<%= node.folded? ? ' hidden' : nil %>">
<% unless node.folded? %>
<%= render partial: 'node', collection: node.children.includes(:page, :children) %>
<% end %>
</ul>
<%= content_tag :ul, class: "children #{' folded' if node.folded?}", data: { node_id: node.id } do %>
<%= render partial: 'node', collection: node.children.includes(:page, :children) %>
<% end %>
</li>
<% end %>
18 changes: 2 additions & 16 deletions app/views/alchemy/admin/nodes/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -43,20 +43,6 @@
</div>

<script>
$('.nodes_tree').on('click', '.node_folder', function() {
var $this = $(this)
var node_id = $this.data('node-id')
var url = '<%= alchemy.toggle_admin_node_path(id: ":id") %>'.replace(':id', node_id)
var $children = $this.closest('li').find('> .children')
$this.find('> i').
toggleClass('fa-plus-square').
toggleClass('fa-minus-square')
$children.toggleClass('hidden')
$.ajax(url, { method: 'PATCH' }).then(function (nodes) {
if ($children.children().length === 0) {
$children.append(nodes)
}
})
return false
})
Alchemy.NodeTree.init()

</script>
4 changes: 2 additions & 2 deletions spec/controllers/alchemy/admin/nodes_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@ module Alchemy

render_views

it "returns nodes children" do
it "returns success" do
patch :toggle, params: { id: node.id }
expect(response.body).to have_selector('li .sitemap_node')
expect(response).to be_successful
end
end
end
Expand Down

0 comments on commit 074882e

Please sign in to comment.