diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js index ae7e1a26..fe3a84c4 100644 --- a/app/assets/javascripts/admin.js +++ b/app/assets/javascripts/admin.js @@ -54,41 +54,6 @@ Helpy.admin = function(){ } }); - $('div.adoptable').sortable({ - revert: true, - }); - $('div.category-row').droppable({ - classes: { - "ui-droppable-hover": "ui-state-active" - }, - tolerance: "intersect", - drop: function (event, ui) { - var parentId = $(this).data('obj-id'); - var childId = ui.draggable.data('obj-id'); - $.ajax({ - type: 'PATCH', - url: '/admin/categories/' + childId + '/set_parent', - dataType: 'json', - data: { id: childId, parent_id: parentId, remote: true }, - complete: function (data) { - $('.front-categories').html(data.responseText); - Helpy.ready(); - Helpy.track(); - Helpy.admin(); - } - }); - } - }); - - $('.adoptable').off().on('mousein', function(){ - // $(this).next('.move-down').show(); - // $('#root-').show(); - }); - $('.adoptable').off().on('mouseout', function(){ - // $(this).next('.move-down').hide(); - // $('#root-').hide(); - }); - $('.settings-link').off().on('click', function(){ // Clean up any select-styled links diff --git a/app/controllers/admin/categories_controller.rb b/app/controllers/admin/categories_controller.rb index b67a4f97..626954ff 100644 --- a/app/controllers/admin/categories_controller.rb +++ b/app/controllers/admin/categories_controller.rb @@ -57,23 +57,9 @@ class Admin::CategoriesController < Admin::BaseController end end - def set_parent - @child = Category.find(params[:id]) - if params[:parent_id].present? - @parent = Category.find(params[:parent_id]) || nil - @child.parent_id = @parent.id - else - @child.parent_id = nil - end - - if @child.save - @public_categories = Category.roots.publicly.featured.ordered.includes(:docs) - respond_to do |format| - format.html { - render partial: 'cat', collection: @public_categories - } - end - end + def reorganize + Category.reorganize(params[:structure]) + render nothing: true end private diff --git a/app/models/category.rb b/app/models/category.rb index f1f838e4..e57d0c8a 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -79,4 +79,15 @@ class Category < ActiveRecord::Base RebuildSearchJob.perform_later end + def self.reorganize(structure, parent_id=nil, parent_rank=0) + logger.info structure + structure.each_with_index do |s,i| + category = Category.find(s[1]["id"]) + category.rank = parent_rank+i+1 + category.parent_id = parent_id + category.save! + Category.reorganize(s[1]["children"], category.id, category.rank*10) if s[1]["children"].present? + end + end + end diff --git a/app/themes/singular/views/categories/show.html.erb b/app/themes/singular/views/categories/show.html.erb index c946097b..619fbcc4 100644 --- a/app/themes/singular/views/categories/show.html.erb +++ b/app/themes/singular/views/categories/show.html.erb @@ -2,7 +2,7 @@ <% meta_tag :description, "#{@category.meta_description}" %> <% meta_tag :keywords, "#{@category.keywords}" %>
- <% Category.children_of(@category).each do |category| %> + <% Category.children_of(@category).ordered.each do |category| %> <%# if category.docs.all.with_translations(I18n.locale).count > 0 %> <%= render partial: 'categories/category_panel', object: category %> <%# end %> diff --git a/app/views/admin/categories/_cat.html.erb b/app/views/admin/categories/_cat.html.erb index f9c812a3..0236aaf6 100644 --- a/app/views/admin/categories/_cat.html.erb +++ b/app/views/admin/categories/_cat.html.erb @@ -1,29 +1,19 @@ -
+
  • -
    +
    <%= content_tag :span, class: 'more-important' do - cat.ancestors.count.times do - concat " ".html_safe - concat " ".html_safe - concat " ".html_safe - end - concat link_to("#{cat.name} (#{cat.id})", admin_category_path(cat.id)) + link_to("#{cat.name}", admin_category_path(cat.id)) end %>
    <%= content_tag :span do - cat.ancestors.count.times do - concat " ".html_safe - concat " ".html_safe - concat " ".html_safe - end concat cat.meta_description end %>
    @@ -44,7 +34,11 @@
    - <% cat.children.each do |child| %> - <%= render partial: 'admin/categories/cat', object: child %> + <% if cat.children.size > 0 %> +
      + <% cat.children.ordered.each do |child| %> + <%= render partial: 'admin/categories/cat', object: child %> + <% end %> +
    <% end %> -
    \ No newline at end of file +
  • \ No newline at end of file diff --git a/app/views/admin/categories/index.html.erb b/app/views/admin/categories/index.html.erb index 924740a2..e3470e9d 100644 --- a/app/views/admin/categories/index.html.erb +++ b/app/views/admin/categories/index.html.erb @@ -1,23 +1,45 @@ <% title "#{admin_title} #{t(:content, default: 'Content')}" %> - +<%= javascript_include_tag "jquery.nestable.js", "data-turbolinks-track" => true %> @@ -39,11 +61,11 @@ $(document).ready(function(){ <%= t(:front_page_categories, default: 'Front Page Categories') %>
    -
    -
    -
    - <%= render partial: 'cat', collection: @public_categories %> +
    +
      + <%= render partial: 'cat', collection: @public_categories %> +
    @@ -54,7 +76,7 @@ $(document).ready(function(){ <%= t(:other_categories, default: 'Other Categories') %> -
    +
    <%= render partial: 'cat', collection: @public_nonfeatured_categories %>
    @@ -65,9 +87,28 @@ $(document).ready(function(){ <%= t(:internal_categories, default: 'Private/Internal Categories') %> -
    +
    <%= render partial: 'cat', collection: @internal_categories %>
    + + \ No newline at end of file diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index c5326ebb..76f78dd5 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -19,4 +19,5 @@ Rails.application.config.assets.precompile += %w( rtl.js ) Rails.application.config.assets.precompile += %w( mousetrap.min.js ) Rails.application.config.assets.precompile += %w( shortcuts.js ) Rails.application.config.assets.precompile += %w( agent_assistant.js ) +Rails.application.config.assets.precompile += %w( jquery.nestable.js ) Rails.application.config.assets.precompile += %w( foundation_emails.css ) diff --git a/config/routes.rb b/config/routes.rb index d3b4bb37..f6c8811c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -143,9 +143,9 @@ Rails.application.routes.draw do get 'internal_docs/search' => 'internal_categories#search', as: :internal_docs_search + patch 'categories/reorganize' => 'categories#reorganize', as: :reorganize resources :categories do resources :docs, except: [:index, :show] - patch 'set_parent' => 'categories#set_parent', as: :set_parent end diff --git a/vendor/assets/javascripts/jquery.nestable.js b/vendor/assets/javascripts/jquery.nestable.js new file mode 100644 index 00000000..2176261e --- /dev/null +++ b/vendor/assets/javascripts/jquery.nestable.js @@ -0,0 +1,461 @@ +/*! + * Nestable jQuery Plugin - Copyright (c) 2012 David Bushell - http://dbushell.com/ + * Dual-licensed under the BSD or MIT licenses + */ +; (function ($, window, document, undefined) { + var hasTouch = 'ontouchstart' in document; + + /** + * Detect CSS pointer-events property + * events are normally disabled on the dragging element to avoid conflicts + * https://github.com/ausi/Feature-detection-technique-for-pointer-events/blob/master/modernizr-pointerevents.js + */ + var hasPointerEvents = (function () { + var el = document.createElement('div'), + docEl = document.documentElement; + if (!('pointerEvents' in el.style)) { + return false; + } + el.style.pointerEvents = 'auto'; + el.style.pointerEvents = 'x'; + docEl.appendChild(el); + var supports = window.getComputedStyle && window.getComputedStyle(el, '').pointerEvents === 'auto'; + docEl.removeChild(el); + return !!supports; + })(); + + var defaults = { + listNodeName: 'ol', + itemNodeName: 'li', + rootClass: 'dd', + listClass: 'dd-list', + itemClass: 'dd-item', + dragClass: 'dd-dragel', + handleClass: 'dd-handle', + collapsedClass: 'dd-collapsed', + placeClass: 'dd-placeholder', + noDragClass: 'dd-nodrag', + emptyClass: 'dd-empty', + expandBtnHTML: '', + collapseBtnHTML: '', + group: 0, + maxDepth: 5, + threshold: 20 + }; + + function Plugin(element, options) { + this.w = $(document); + this.el = $(element); + this.options = $.extend({}, defaults, options); + this.init(); + } + + Plugin.prototype = { + + init: function () { + var list = this; + + list.reset(); + + list.el.data('nestable-group', this.options.group); + + list.placeEl = $('
    '); + + $.each(this.el.find(list.options.itemNodeName), function (k, el) { + list.setParent($(el)); + }); + + list.el.on('click', 'button', function (e) { + if (list.dragEl) { + return; + } + var target = $(e.currentTarget), + action = target.data('action'), + item = target.parent(list.options.itemNodeName); + if (action === 'collapse') { + list.collapseItem(item); + } + if (action === 'expand') { + list.expandItem(item); + } + }); + + var onStartEvent = function (e) { + var handle = $(e.target); + if (!handle.hasClass(list.options.handleClass)) { + if (handle.closest('.' + list.options.noDragClass).length) { + return; + } + handle = handle.closest('.' + list.options.handleClass); + } + + if (!handle.length || list.dragEl) { + return; + } + + list.isTouch = /^touch/.test(e.type); + if (list.isTouch && e.touches.length !== 1) { + return; + } + + e.preventDefault(); + list.dragStart(e.touches ? e.touches[0] : e); + }; + + var onMoveEvent = function (e) { + if (list.dragEl) { + e.preventDefault(); + list.dragMove(e.touches ? e.touches[0] : e); + } + }; + + var onEndEvent = function (e) { + if (list.dragEl) { + e.preventDefault(); + list.dragStop(e.touches ? e.touches[0] : e); + } + }; + + if (hasTouch) { + list.el[0].addEventListener('touchstart', onStartEvent, false); + window.addEventListener('touchmove', onMoveEvent, false); + window.addEventListener('touchend', onEndEvent, false); + window.addEventListener('touchcancel', onEndEvent, false); + } + + list.el.on('mousedown', onStartEvent); + list.w.on('mousemove', onMoveEvent); + list.w.on('mouseup', onEndEvent); + + }, + + serialize: function () { + var data, + depth = 0, + list = this; + step = function (level, depth) { + var array = [], + items = level.children(list.options.itemNodeName); + items.each(function () { + var li = $(this), + item = $.extend({}, li.data()), + sub = li.children(list.options.listNodeName); + if (sub.length) { + item.children = step(sub, depth + 1); + } + array.push(item); + }); + return array; + }; + data = step(list.el.find(list.options.listNodeName).first(), depth); + return data; + }, + + serialise: function () { + return this.serialize(); + }, + + reset: function () { + this.mouse = { + offsetX: 0, + offsetY: 0, + startX: 0, + startY: 0, + lastX: 0, + lastY: 0, + nowX: 0, + nowY: 0, + distX: 0, + distY: 0, + dirAx: 0, + dirX: 0, + dirY: 0, + lastDirX: 0, + lastDirY: 0, + distAxX: 0, + distAxY: 0 + }; + this.isTouch = false; + this.moving = false; + this.dragEl = null; + this.dragRootEl = null; + this.dragDepth = 0; + this.hasNewRoot = false; + this.pointEl = null; + }, + + expandItem: function (li) { + li.removeClass(this.options.collapsedClass); + li.children('[data-action="expand"]').hide(); + li.children('[data-action="collapse"]').show(); + li.children(this.options.listNodeName).show(); + }, + + collapseItem: function (li) { + var lists = li.children(this.options.listNodeName); + if (lists.length) { + li.addClass(this.options.collapsedClass); + li.children('[data-action="collapse"]').hide(); + li.children('[data-action="expand"]').show(); + li.children(this.options.listNodeName).hide(); + } + }, + + expandAll: function () { + var list = this; + list.el.find(list.options.itemNodeName).each(function () { + list.expandItem($(this)); + }); + }, + + collapseAll: function () { + var list = this; + list.el.find(list.options.itemNodeName).each(function () { + list.collapseItem($(this)); + }); + }, + + setParent: function (li) { + if (li.children(this.options.listNodeName).length) { + li.prepend($(this.options.expandBtnHTML)); + li.prepend($(this.options.collapseBtnHTML)); + } + li.children('[data-action="expand"]').hide(); + }, + + unsetParent: function (li) { + li.removeClass(this.options.collapsedClass); + li.children('[data-action]').remove(); + li.children(this.options.listNodeName).remove(); + }, + + dragStart: function (e) { + var mouse = this.mouse, + target = $(e.target), + dragItem = target.closest(this.options.itemNodeName); + + this.placeEl.css('height', dragItem.height()); + + mouse.offsetX = e.offsetX !== undefined ? e.offsetX : e.pageX - target.offset().left; + mouse.offsetY = e.offsetY !== undefined ? e.offsetY : e.pageY - target.offset().top; + mouse.startX = mouse.lastX = e.pageX; + mouse.startY = mouse.lastY = e.pageY; + + this.dragRootEl = this.el; + + this.dragEl = $(document.createElement(this.options.listNodeName)).addClass(this.options.listClass + ' ' + this.options.dragClass); + this.dragEl.css('width', dragItem.width()); + + dragItem.after(this.placeEl); + dragItem[0].parentNode.removeChild(dragItem[0]); + dragItem.appendTo(this.dragEl); + + $(document.body).append(this.dragEl); + this.dragEl.css({ + 'left': e.pageX - mouse.offsetX, + 'top': e.pageY - mouse.offsetY + }); + // total depth of dragging item + var i, depth, + items = this.dragEl.find(this.options.itemNodeName); + for (i = 0; i < items.length; i++) { + depth = $(items[i]).parents(this.options.listNodeName).length; + if (depth > this.dragDepth) { + this.dragDepth = depth; + } + } + }, + + dragStop: function (e) { + var el = this.dragEl.children(this.options.itemNodeName).first(); + el[0].parentNode.removeChild(el[0]); + this.placeEl.replaceWith(el); + + this.dragEl.remove(); + this.el.trigger('change'); + if (this.hasNewRoot) { + this.dragRootEl.trigger('change'); + } + this.reset(); + }, + + dragMove: function (e) { + var list, parent, prev, next, depth, + opt = this.options, + mouse = this.mouse; + + this.dragEl.css({ + 'left': e.pageX - mouse.offsetX, + 'top': e.pageY - mouse.offsetY + }); + + // mouse position last events + mouse.lastX = mouse.nowX; + mouse.lastY = mouse.nowY; + // mouse position this events + mouse.nowX = e.pageX; + mouse.nowY = e.pageY; + // distance mouse moved between events + mouse.distX = mouse.nowX - mouse.lastX; + mouse.distY = mouse.nowY - mouse.lastY; + // direction mouse was moving + mouse.lastDirX = mouse.dirX; + mouse.lastDirY = mouse.dirY; + // direction mouse is now moving (on both axis) + mouse.dirX = mouse.distX === 0 ? 0 : mouse.distX > 0 ? 1 : -1; + mouse.dirY = mouse.distY === 0 ? 0 : mouse.distY > 0 ? 1 : -1; + // axis mouse is now moving on + var newAx = Math.abs(mouse.distX) > Math.abs(mouse.distY) ? 1 : 0; + + // do nothing on first move + if (!mouse.moving) { + mouse.dirAx = newAx; + mouse.moving = true; + return; + } + + // calc distance moved on this axis (and direction) + if (mouse.dirAx !== newAx) { + mouse.distAxX = 0; + mouse.distAxY = 0; + } else { + mouse.distAxX += Math.abs(mouse.distX); + if (mouse.dirX !== 0 && mouse.dirX !== mouse.lastDirX) { + mouse.distAxX = 0; + } + mouse.distAxY += Math.abs(mouse.distY); + if (mouse.dirY !== 0 && mouse.dirY !== mouse.lastDirY) { + mouse.distAxY = 0; + } + } + mouse.dirAx = newAx; + + /** + * move horizontal + */ + if (mouse.dirAx && mouse.distAxX >= opt.threshold) { + // reset move distance on x-axis for new phase + mouse.distAxX = 0; + prev = this.placeEl.prev(opt.itemNodeName); + // increase horizontal level if previous sibling exists and is not collapsed + if (mouse.distX > 0 && prev.length && !prev.hasClass(opt.collapsedClass)) { + // cannot increase level when item above is collapsed + list = prev.find(opt.listNodeName).last(); + // check if depth limit has reached + depth = this.placeEl.parents(opt.listNodeName).length; + if (depth + this.dragDepth <= opt.maxDepth) { + // create new sub-level if one doesn't exist + if (!list.length) { + list = $('<' + opt.listNodeName + '/>').addClass(opt.listClass); + list.append(this.placeEl); + prev.append(list); + this.setParent(prev); + } else { + // else append to next level up + list = prev.children(opt.listNodeName).last(); + list.append(this.placeEl); + } + } + } + // decrease horizontal level + if (mouse.distX < 0) { + // we can't decrease a level if an item preceeds the current one + next = this.placeEl.next(opt.itemNodeName); + if (!next.length) { + parent = this.placeEl.parent(); + this.placeEl.closest(opt.itemNodeName).after(this.placeEl); + if (!parent.children().length) { + this.unsetParent(parent.parent()); + } + } + } + } + + var isEmpty = false; + + // find list item under cursor + if (!hasPointerEvents) { + this.dragEl[0].style.visibility = 'hidden'; + } + this.pointEl = $(document.elementFromPoint(e.pageX - document.body.scrollLeft, e.pageY - (window.pageYOffset || document.documentElement.scrollTop))); + if (!hasPointerEvents) { + this.dragEl[0].style.visibility = 'visible'; + } + if (this.pointEl.hasClass(opt.handleClass)) { + this.pointEl = this.pointEl.parent(opt.itemNodeName); + } + if (this.pointEl.hasClass(opt.emptyClass)) { + isEmpty = true; + } + else if (!this.pointEl.length || !this.pointEl.hasClass(opt.itemClass)) { + return; + } + + // find parent list of item under cursor + var pointElRoot = this.pointEl.closest('.' + opt.rootClass), + isNewRoot = this.dragRootEl.data('nestable-id') !== pointElRoot.data('nestable-id'); + + /** + * move vertical + */ + if (!mouse.dirAx || isNewRoot || isEmpty) { + // check if groups match if dragging over new root + if (isNewRoot && opt.group !== pointElRoot.data('nestable-group')) { + return; + } + // check depth limit + depth = this.dragDepth - 1 + this.pointEl.parents(opt.listNodeName).length; + if (depth > opt.maxDepth) { + return; + } + var before = e.pageY < (this.pointEl.offset().top + this.pointEl.height() / 2); + parent = this.placeEl.parent(); + // if empty create new list to replace empty placeholder + if (isEmpty) { + list = $(document.createElement(opt.listNodeName)).addClass(opt.listClass); + list.append(this.placeEl); + this.pointEl.replaceWith(list); + } + else if (before) { + this.pointEl.before(this.placeEl); + } + else { + this.pointEl.after(this.placeEl); + } + if (!parent.children().length) { + this.unsetParent(parent.parent()); + } + if (!this.dragRootEl.find(opt.itemNodeName).length) { + this.dragRootEl.append('
    '); + } + // parent root list has changed + if (isNewRoot) { + this.dragRootEl = pointElRoot; + this.hasNewRoot = this.el[0] !== this.dragRootEl[0]; + } + } + } + + }; + + $.fn.nestable = function (params) { + var lists = this, + retval = this; + + lists.each(function () { + var plugin = $(this).data("nestable"); + + if (!plugin) { + $(this).data("nestable", new Plugin(this, params)); + $(this).data("nestable-id", new Date().getTime()); + } else { + if (typeof params === 'string' && typeof plugin[params] === 'function') { + retval = plugin[params](); + } + } + }); + + return retval || lists; + }; + +})(window.jQuery || window.Zepto, window, document); \ No newline at end of file