Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sortable menus #1758

Merged
merged 14 commits into from
Mar 25, 2020
Merged
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.')
tvdeyen marked this conversation as resolved.
Show resolved Hide resolved
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() {
tvdeyen marked this conversation as resolved.
Show resolved Hide resolved
Alchemy.on('click', '.nodes_tree', '.node_folder', function(evt) {
tvdeyen marked this conversation as resolved.
Show resolved Hide resolved
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) {
tvdeyen marked this conversation as resolved.
Show resolved Hide resolved
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) {
tvdeyen marked this conversation as resolved.
Show resolved Hide resolved
tvdeyen marked this conversation as resolved.
Show resolved Hide resolved
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>
tvdeyen marked this conversation as resolved.
Show resolved Hide resolved
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 {
tvdeyen marked this conversation as resolved.
Show resolved Hide resolved
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])
tvdeyen marked this conversation as resolved.
Show resolved Hide resolved
mamhoff marked this conversation as resolved.
Show resolved Hide resolved
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() {
mamhoff marked this conversation as resolved.
Show resolved Hide resolved
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()
mamhoff marked this conversation as resolved.
Show resolved Hide resolved

</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 @@ -18,11 +18,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 @@ -153,6 +149,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