Skip to content

Commit

Permalink
Merge pull request #1758 from mamhoff/sortable-menus
Browse files Browse the repository at this point in the history
Sortable menus
  • Loading branch information
tvdeyen committed Mar 30, 2020
1 parent 8a48b08 commit 9f7a122
Show file tree
Hide file tree
Showing 17 changed files with 321 additions and 85 deletions.
3 changes: 3 additions & 0 deletions app/assets/javascripts/alchemy/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
//= require requestAnimationFrame
//= require select2
//= require handlebars
//= require sortable/Sortable.min
//= require alchemy/templates
//= require alchemy/alchemy.base
//= require alchemy/alchemy.utils
//= require alchemy/alchemy.autocomplete
//= require alchemy/alchemy.browser
//= require alchemy/alchemy.buttons
Expand All @@ -39,6 +41,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
66 changes: 66 additions & 0 deletions app/assets/javascripts/alchemy/alchemy.node_tree.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
Alchemy.NodeTree = {
onFinishDragging: function (evt) {
var url = Alchemy.routes.move_api_node_path(evt.item.dataset.id)
var data = {
target_parent_id: evt.to.dataset.nodeId,
new_position: evt.newIndex
};
var ajax = Alchemy.ajax('PATCH', url, data)

ajax.then(function(response) {
Alchemy.growl('Successfully moved menu item.')
Alchemy.NodeTree.displayNodeFolders()
}).catch(function() {
Alchemy.growl(error.message || error);
})
},

displayNodeFolders: function () {
document.querySelectorAll('li.menu-item').forEach(function (el) {
var leftIconArea = el.querySelector('.nodes_tree-left_images')
var list = el.querySelector('ul')
var node = { folded: el.dataset.folded === 'true', id: el.dataset.id }

if (list.children.length > 0 || node.folded ) {
leftIconArea.innerHTML = HandlebarsTemplates.node_folder({ node: node })
} else {
leftIconArea.innerHTML = ' '
}
});
},

handleNodeFolders: function() {
Alchemy.on('click', '.nodes_tree', '.node_folder', function(evt) {
var nodeId = this.dataset.nodeId
var menu_item = this.closest('li.menu-item')
var url = Alchemy.routes.toggle_folded_api_node_path(nodeId)
var list = menu_item.querySelector('.children')
var ajax = Alchemy.ajax('PATCH', url)

ajax.then(function() {
list.classList.toggle('folded')
menu_item.dataset.folded = menu_item.dataset.folded == 'true' ? 'false' : 'true'
Alchemy.NodeTree.displayNodeFolders();
}).catch(function(error){
Alchemy.growl(error.message || error);
});
});
},

init: function() {
this.handleNodeFolders()
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
});
});
}
}
45 changes: 45 additions & 0 deletions app/assets/javascripts/alchemy/alchemy.utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
Alchemy.on = function (eventName, baseSelector, targetSelector, callback) {
var baseNode = document.querySelector(baseSelector)
baseNode.addEventListener(eventName, function (evt) {
var targets = Array.from(baseNode.querySelectorAll(targetSelector))
var currentNode = evt.target
while (currentNode !== baseNode) {
if (targets.includes(currentNode)) {
callback.call(currentNode, evt)
return
}
currentNode = currentNode.parentElement
}
});
}

Alchemy.ajax = function(method, url, data) {
var xhr = new XMLHttpRequest()
var token = document.querySelector('meta[name="csrf-token"]').attributes.content.textContent
var promise = new Promise(function (resolve, reject) {
xhr.onload = function() {
try {
resolve({
data: JSON.parse(xhr.responseText),
status: xhr.status
})
} catch (error) {
reject(new Error(JSON.parse(xhr.responseText).error))
}
};
xhr.onerror = function() {
reject(new Error(xhr.statusText))
}
});
xhr.open(method, url);
xhr.setRequestHeader('Content-type', 'application/json; charset=utf-8');
xhr.setRequestHeader('Accept', 'application/json');
xhr.setRequestHeader('X-CSRF-Token', token)
if (data) {
xhr.send(JSON.stringify(data))
} else {
xhr.send()
}

return promise
}
1 change: 1 addition & 0 deletions app/assets/javascripts/alchemy/templates/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
//= require alchemy/templates/spinner
//= require alchemy/templates/page
//= require alchemy/templates/node_folder
3 changes: 3 additions & 0 deletions app/assets/javascripts/alchemy/templates/node_folder.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<a class="node_folder" data-node-id="{{ node.id }}">
<i class="far fa-{{#if node.folded }}plus{{else}}minus{{/if}}-square fa-fw"></i>
</a>
5 changes: 5 additions & 0 deletions app/assets/stylesheets/alchemy/nodes.scss
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@
ul {
margin: 0;
padding: 0;

.folded > li {
display: none;
}
}

li {
Expand Down Expand Up @@ -106,6 +110,7 @@
text-decoration: none;
overflow: hidden;
background-color: $sitemap-page-background-color;
cursor: move;

&.without-status {
@include border-right-radius($default-border-radius);
Expand Down
10 changes: 0 additions & 10 deletions app/controllers/alchemy/admin/nodes_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,6 @@ def new
)
end

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
end

private

def resource_params
Expand Down
29 changes: 29 additions & 0 deletions app/controllers/alchemy/api/nodes_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# frozen_string_literal: true

module Alchemy
class Api::NodesController < Api::BaseController
before_action :load_node
before_action :authorize_access, only: [:move, :toggle_folded]

def move
target_parent_node = Node.find(params[:target_parent_id])
@node.move_to_child_with_index(target_parent_node, params[:new_position])
render json: @node, serializer: NodeSerializer
end

def toggle_folded
@node.update(folded: !@node.folded)
render json: @node, serializer: NodeSerializer
end

private

def load_node
@node = Node.find(params[:id])
end

def authorize_access
authorize! :update, @node
end
end
end
12 changes: 12 additions & 0 deletions app/serializers/alchemy/node_serializer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

module Alchemy
class NodeSerializer < ActiveModel::Serializer
attributes :id,
:name,
:lft,
:rgt,
:url,
:parent_id
end
end
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>
8 changes: 8 additions & 0 deletions app/views/alchemy/admin/partials/_routes.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@
return '<%= alchemy.fold_admin_element_path(id: 1) %>'.replace(/1/, id);
},

toggle_folded_api_node_path: function(id) {
return '<%= alchemy.toggle_folded_api_node_path(id: 1) %>'.replace(/1/, id);
},

move_api_node_path: function(id) {
return '<%= alchemy.move_api_node_path(id: 1) %>'.replace(/1/, id);
},

order_admin_elements_path: '<%= alchemy.order_admin_elements_path %>',
order_admin_pages_path: '<%= alchemy.order_admin_pages_path %>',
link_admin_pages_path: '<%= alchemy.link_admin_pages_path %>',
Expand Down
13 changes: 8 additions & 5 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,7 @@
namespace :admin, {path: Alchemy.admin_path, constraints: Alchemy.admin_constraints} do
resources :contents, only: [:create]

resources :nodes do
member do
patch :toggle
end
end
resources :nodes

resources :pages do
resources :elements
Expand Down Expand Up @@ -152,6 +148,13 @@

get '/pages/*urlname(.:format)' => 'pages#show', as: 'page'
get '/admin/pages/:id(.:format)' => 'pages#show', as: 'preview_page'

resources :nodes, only: [] do
member do
patch :move
patch :toggle_folded
end
end
end

get '/:locale' => 'pages#index',
Expand Down
35 changes: 0 additions & 35 deletions spec/controllers/alchemy/admin/nodes_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,40 +60,5 @@ module Alchemy
end
end
end

describe '#toggle' do
context 'with expanded node' do
let(:node) { create(:alchemy_node, folded: false) }

it "folds node" do
expect {
patch :toggle, params: { id: node.id }
}.to change { node.reload.folded }.to(true)
end
end

context 'with folded node' do
let(:node) { create(:alchemy_node, folded: true) }

it "expands node" do
expect {
patch :toggle, params: { id: node.id }
}.to change { node.reload.folded }.to(false)
end

context 'with node having children' do
before do
create(:alchemy_node, parent: node)
end

render_views

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

0 comments on commit 9f7a122

Please sign in to comment.