From fd52d001870627b9b87c3a755365c055557b3b83 Mon Sep 17 00:00:00 2001 From: Christophe Vilayphiou Date: Fri, 11 May 2012 10:00:45 +0800 Subject: [PATCH] Add drag and drop to structure side_bar --- Gemfile | 1 + Gemfile.lock | 4 + .../lib/jquery.ui.nestedSortable.js | 391 ++++++++++++++++++ app/assets/javascripts/site_editor.js | 6 +- app/assets/stylesheets/sidebar.css.erb | 6 +- app/controllers/admin/items_controller.rb | 4 + app/helpers/admin/item_helper.rb | 22 +- .../admin/items/_site_map_left_bar.html.erb | 29 +- app/views/layouts/site_editor.html.erb | 4 +- config/routes.rb | 1 + 10 files changed, 457 insertions(+), 11 deletions(-) create mode 100755 app/assets/javascripts/lib/jquery.ui.nestedSortable.js diff --git a/Gemfile b/Gemfile index d6129092..24ee0383 100644 --- a/Gemfile +++ b/Gemfile @@ -10,6 +10,7 @@ gem 'devise', '1.5.3' gem 'exception_notification' # Send error trace gem 'execjs' gem 'jquery-rails' +gem 'jquery-ui-rails' gem 'kaminari', :git => 'git://github.com/amatsuda/kaminari.git' diff --git a/Gemfile.lock b/Gemfile.lock index 16acce21..e850e52b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -96,6 +96,9 @@ GEM jquery-rails (1.0.19) railties (~> 3.0) thor (~> 0.14) + jquery-ui-rails (0.4.0) + jquery-rails + railties (>= 3.1.0) json (1.6.5) linecache19 (0.5.12) ruby_core_source (>= 0.1.4) @@ -269,6 +272,7 @@ DEPENDENCIES execjs factory_girl_rails jquery-rails + jquery-ui-rails kaminari! mini_magick mongo_session_store-rails3 diff --git a/app/assets/javascripts/lib/jquery.ui.nestedSortable.js b/app/assets/javascripts/lib/jquery.ui.nestedSortable.js new file mode 100755 index 00000000..aad83af5 --- /dev/null +++ b/app/assets/javascripts/lib/jquery.ui.nestedSortable.js @@ -0,0 +1,391 @@ +/* + * jQuery UI Nested Sortable + * v 1.3.4 / 28 apr 2011 + * http://mjsarfatti.com/sandbox/nestedSortable + * + * Depends: + * jquery.ui.sortable.js 1.8+ + * + * License CC BY-SA 3.0 + * Copyright 2010-2011, Manuele J Sarfatti + */ + +(function($) { + + $.widget("ui.nestedSortable", $.extend({}, $.ui.sortable.prototype, { + + options: { + tabSize: 20, + disableNesting: 'ui-nestedSortable-no-nesting', + errorClass: 'ui-nestedSortable-error', + listType: 'ol', + maxLevels: 0, + revertOnError: 1 + }, + + _create: function() { + this.element.data('sortable', this.element.data('nestedSortable')); + return $.ui.sortable.prototype._create.apply(this, arguments); + }, + + destroy: function() { + this.element + .removeData("nestedSortable") + .unbind(".nestedSortable"); + return $.ui.sortable.prototype.destroy.apply(this, arguments); + }, + + _mouseDrag: function(event) { + + //Compute the helpers position + this.position = this._generatePosition(event); + this.positionAbs = this._convertPositionTo("absolute"); + + if (!this.lastPositionAbs) { + this.lastPositionAbs = this.positionAbs; + } + + //Do scrolling + if(this.options.scroll) { + var o = this.options, scrolled = false; + if(this.scrollParent[0] != document && this.scrollParent[0].tagName != 'HTML') { + + if((this.overflowOffset.top + this.scrollParent[0].offsetHeight) - event.pageY < o.scrollSensitivity) + this.scrollParent[0].scrollTop = scrolled = this.scrollParent[0].scrollTop + o.scrollSpeed; + else if(event.pageY - this.overflowOffset.top < o.scrollSensitivity) + this.scrollParent[0].scrollTop = scrolled = this.scrollParent[0].scrollTop - o.scrollSpeed; + + if((this.overflowOffset.left + this.scrollParent[0].offsetWidth) - event.pageX < o.scrollSensitivity) + this.scrollParent[0].scrollLeft = scrolled = this.scrollParent[0].scrollLeft + o.scrollSpeed; + else if(event.pageX - this.overflowOffset.left < o.scrollSensitivity) + this.scrollParent[0].scrollLeft = scrolled = this.scrollParent[0].scrollLeft - o.scrollSpeed; + + } else { + + if(event.pageY - $(document).scrollTop() < o.scrollSensitivity) + scrolled = $(document).scrollTop($(document).scrollTop() - o.scrollSpeed); + else if($(window).height() - (event.pageY - $(document).scrollTop()) < o.scrollSensitivity) + scrolled = $(document).scrollTop($(document).scrollTop() + o.scrollSpeed); + + if(event.pageX - $(document).scrollLeft() < o.scrollSensitivity) + scrolled = $(document).scrollLeft($(document).scrollLeft() - o.scrollSpeed); + else if($(window).width() - (event.pageX - $(document).scrollLeft()) < o.scrollSensitivity) + scrolled = $(document).scrollLeft($(document).scrollLeft() + o.scrollSpeed); + + } + + if(scrolled !== false && $.ui.ddmanager && !o.dropBehaviour) + $.ui.ddmanager.prepareOffsets(this, event); + } + + //Regenerate the absolute position used for position checks + this.positionAbs = this._convertPositionTo("absolute"); + + //Set the helper position + if(!this.options.axis || this.options.axis != "y") this.helper[0].style.left = this.position.left+'px'; + if(!this.options.axis || this.options.axis != "x") this.helper[0].style.top = this.position.top+'px'; + + //Rearrange + for (var i = this.items.length - 1; i >= 0; i--) { + + //Cache variables and intersection, continue if no intersection + var item = this.items[i], itemElement = item.item[0], intersection = this._intersectsWithPointer(item); + if (!intersection) continue; + + if(itemElement != this.currentItem[0] //cannot intersect with itself + && this.placeholder[intersection == 1 ? "next" : "prev"]()[0] != itemElement //no useless actions that have been done before + && !$.contains(this.placeholder[0], itemElement) //no action if the item moved is the parent of the item checked + && (this.options.type == 'semi-dynamic' ? !$.contains(this.element[0], itemElement) : true) + //&& itemElement.parentNode == this.placeholder[0].parentNode // only rearrange items within the same container + ) { + + $(itemElement).mouseenter(); + + this.direction = intersection == 1 ? "down" : "up"; + + if (this.options.tolerance == "pointer" || this._intersectsWithSides(item)) { + $(itemElement).mouseleave(); + this._rearrange(event, item); + } else { + break; + } + + // Clear emtpy ul's/ol's + this._clearEmpty(itemElement); + + this._trigger("change", event, this._uiHash()); + break; + } + } + + var parentItem = (this.placeholder[0].parentNode.parentNode + && $(this.placeholder[0].parentNode.parentNode).closest('.ui-sortable').length) + ? $(this.placeholder[0].parentNode.parentNode) + : null, + level = this._getLevel(this.placeholder), + childLevels = this._getChildLevels(this.helper), + previousItem = this.placeholder[0].previousSibling ? $(this.placeholder[0].previousSibling) : null; + + if (previousItem != null) { + while (previousItem[0].nodeName.toLowerCase() != 'li' || previousItem[0] == this.currentItem[0]) { + if (previousItem[0].previousSibling) { + previousItem = $(previousItem[0].previousSibling); + } else { + previousItem = null; + break; + } + } + } + + newList = document.createElement(o.listType); + + this.beyondMaxLevels = 0; + + // If the item is moved to the left, send it to its parent level + if (parentItem != null && this.positionAbs.left < parentItem.offset().left) { + parentItem.after(this.placeholder[0]); + this._clearEmpty(parentItem[0]); + this._trigger("change", event, this._uiHash()); + } + // If the item is below another one and is moved to the right, make it a children of it + else if (previousItem != null && this.positionAbs.left > previousItem.offset().left + o.tabSize) { + this._isAllowed(previousItem, level+childLevels+1); + if (!previousItem.children(o.listType).length) { + previousItem[0].appendChild(newList); + } + previousItem.children(o.listType)[0].appendChild(this.placeholder[0]); + this._trigger("change", event, this._uiHash()); + } + else { + this._isAllowed(parentItem, level+childLevels); + } + + //Post events to containers + this._contactContainers(event); + + //Interconnect with droppables + if($.ui.ddmanager) $.ui.ddmanager.drag(this, event); + + //Call callbacks + this._trigger('sort', event, this._uiHash()); + + this.lastPositionAbs = this.positionAbs; + return false; + + }, + + _mouseStop: function(event, noPropagation) { + + // If the item is in a position not allowed, send it back + if (this.beyondMaxLevels) { + + this.placeholder.removeClass(this.options.errorClass); + + if (this.options.revertOnError) { + if (this.domPosition.prev) { + $(this.domPosition.prev).after(this.placeholder); + } else { + $(this.domPosition.parent).prepend(this.placeholder); + } + this._trigger("revert", event, this._uiHash()); + } else { + var parent = this.placeholder.parent().closest(this.options.items); + + for (var i = this.beyondMaxLevels - 1; i > 0; i--) { + parent = parent.parent().closest(this.options.items); + } + + parent.after(this.placeholder); + this._trigger("change", event, this._uiHash()); + } + + } + + // Clean last empty ul/ol + for (var i = this.items.length - 1; i >= 0; i--) { + var item = this.items[i].item[0]; + this._clearEmpty(item); + } + + $.ui.sortable.prototype._mouseStop.apply(this, arguments); + + }, + + serialize: function(o) { + + var items = this._getItemsAsjQuery(o && o.connected), + str = []; o = o || {}; + + $(items).each(function() { + var res = ($(o.item || this).attr(o.attribute || 'id') || '') + .match(o.expression || (/(.+)[-=_](.+)/)), + pid = ($(o.item || this).parent(o.listType) + .parent('li') + .attr(o.attribute || 'id') || '') + .match(o.expression || (/(.+)[-=_](.+)/)); + + if (res) { + str.push(((o.key || res[1]) + '[' + (o.key && o.expression ? res[1] : res[2]) + ']') + + '=' + + (pid ? (o.key && o.expression ? pid[1] : pid[2]) : 'root')); + } + }); + + if(!str.length && o.key) { + str.push(o.key + '='); + } + + return str.join('&'); + + }, + + toHierarchy: function(o) { + + o = o || {}; + var sDepth = o.startDepthCount || 0, + ret = []; + + $(this.element).children('li').each(function () { + var level = _recursiveItems($(this)); + ret.push(level); + }); + + return ret; + + function _recursiveItems(li) { + var id = ($(li).attr(o.attribute || 'id') || '').match(o.expression || (/(.+)[-=_](.+)/)); + if (id) { + var item = {"id" : id[2]}; + if ($(li).children(o.listType).children('li').length > 0) { + item.children = []; + $(li).children(o.listType).children('li').each(function() { + var level = _recursiveItems($(this)); + item.children.push(level); + }); + } + return item; + } + } + }, + + toArray: function(o) { + + o = o || {}; + var sDepth = o.startDepthCount || 0, + ret = [], + left = 2; + + ret.push({ + "item_id": 'root', + "parent_id": 'none', + "depth": sDepth, + "left": '1', + "right": ($('li', this.element).length + 1) * 2 + }); + + $(this.element).children('li').each(function () { + left = _recursiveArray(this, sDepth + 1, left); + }); + + ret = ret.sort(function(a,b){ return (a.left - b.left); }); + + return ret; + + function _recursiveArray(item, depth, left) { + + var right = left + 1, + id, + pid; + + if ($(item).children(o.listType).children('li').length > 0) { + depth ++; + $(item).children(o.listType).children('li').each(function () { + right = _recursiveArray($(this), depth, right); + }); + depth --; + } + + id = ($(item).attr(o.attribute || 'id')).match(o.expression || (/(.+)[-=_](.+)/)); + + if (depth === sDepth + 1) { + pid = 'root'; + } else { + var parentItem = ($(item).parent(o.listType) + .parent('li') + .attr(o.attribute || 'id')) + .match(o.expression || (/(.+)[-=_](.+)/)); + pid = parentItem[2]; + } + + if (id) { + ret.push({"item_id": id[2], "parent_id": pid, "depth": depth, "left": left, "right": right}); + } + + left = right + 1; + return left; + } + + }, + + _clearEmpty: function(item) { + + var emptyList = $(item).children(this.options.listType); + if (emptyList.length && !emptyList.children().length) { + emptyList.remove(); + } + + }, + + _getLevel: function(item) { + + var level = 1; + + if (this.options.listType) { + var list = item.closest(this.options.listType); + while (!list.is('.ui-sortable')) { + level++; + list = list.parent().closest(this.options.listType); + } + } + + return level; + }, + + _getChildLevels: function(parent, depth) { + var self = this, + o = this.options, + result = 0; + depth = depth || 0; + + $(parent).children(o.listType).children(o.items).each(function (index, child) { + result = Math.max(self._getChildLevels(child, depth + 1), result); + }); + + return depth ? result + 1 : result; + }, + + _isAllowed: function(parentItem, levels) { + var o = this.options; + // Are we trying to nest under a no-nest or are we nesting too deep? + if (parentItem == null || !(parentItem.hasClass(o.disableNesting))) { + if (o.maxLevels < levels && o.maxLevels != 0) { + this.placeholder.addClass(o.errorClass); + this.beyondMaxLevels = levels - o.maxLevels; + } else { + this.placeholder.removeClass(o.errorClass); + this.beyondMaxLevels = 0; + } + } else { + this.placeholder.addClass(o.errorClass); + if (o.maxLevels < levels && o.maxLevels != 0) { + this.beyondMaxLevels = levels - o.maxLevels; + } else { + this.beyondMaxLevels = 1; + } + } + } + + })); + + $.ui.nestedSortable.prototype.options = $.extend({}, $.ui.sortable.prototype.options, $.ui.nestedSortable.prototype.options); +})(jQuery); \ No newline at end of file diff --git a/app/assets/javascripts/site_editor.js b/app/assets/javascripts/site_editor.js index 2076f04d..5d77e701 100644 --- a/app/assets/javascripts/site_editor.js +++ b/app/assets/javascripts/site_editor.js @@ -5,6 +5,9 @@ // the compiled file. // //= require jquery +//= require jquery.ui.draggable +//= require jquery.ui.droppable +//= require jquery.ui.sortable //= require jquery_ujs //= require bootstrap //= require jquery.isotope.min @@ -14,4 +17,5 @@ //= require side_bar_history //= require page_edit //= require tinymce-jquery -//= require tinymce_orbit \ No newline at end of file +//= require tinymce_orbit +//= require lib/jquery.ui.nestedSortable.js \ No newline at end of file diff --git a/app/assets/stylesheets/sidebar.css.erb b/app/assets/stylesheets/sidebar.css.erb index 4231d0bf..be15746e 100644 --- a/app/assets/stylesheets/sidebar.css.erb +++ b/app/assets/stylesheets/sidebar.css.erb @@ -6,13 +6,13 @@ .nav-list li { position: relative; } -.nav-list ul { +.nav-list ol { margin-left: 10px; } -.nav-list ul li { +.nav-list ol li { padding: 5px 0; } -.nav-list ul li a { +.nav-list ol li a { font-size: 13px; padding: 3px 0px; display: block; diff --git a/app/controllers/admin/items_controller.rb b/app/controllers/admin/items_controller.rb index 33bc0fd2..eea82f9c 100644 --- a/app/controllers/admin/items_controller.rb +++ b/app/controllers/admin/items_controller.rb @@ -15,6 +15,10 @@ class Admin::ItemsController < ApplicationController @item = get_homepage end end + + def update_position + render :nothing => true + end protected diff --git a/app/helpers/admin/item_helper.rb b/app/helpers/admin/item_helper.rb index 8af4db34..dc78db88 100644 --- a/app/helpers/admin/item_helper.rb +++ b/app/helpers/admin/item_helper.rb @@ -9,8 +9,8 @@ module Admin::ItemHelper when 'Link' dest = admin_link_path(node) end - ret << "" unless node.parent.nil? + # ret << "" unless node.parent.nil? end ret.html_safe end @@ -31,13 +31,27 @@ module Admin::ItemHelper children = parent.ordered_children if !children.entries.blank? ret = '' + ret << "
    " children.each do |child| ret << render_node_and_children(child) end + ret << '
' ret else '' end end -end \ No newline at end of file +end + + +#
    +#
  1. Some content
  2. +#
  3. Some content
    +#
      +#
    1. Some sub-item content
    2. +#
    3. Some sub-item content
    4. +#
    +#
  4. +#
  5. Some content
  6. +#
\ No newline at end of file diff --git a/app/views/admin/items/_site_map_left_bar.html.erb b/app/views/admin/items/_site_map_left_bar.html.erb index e82b4988..69e7851c 100644 --- a/app/views/admin/items/_site_map_left_bar.html.erb +++ b/app/views/admin/items/_site_map_left_bar.html.erb @@ -1 +1,28 @@ -<%= render_node_and_children(Item.first(:conditions => {:parent_id => nil})) %> \ No newline at end of file +<%= render_node_and_children(Item.first(:conditions => {:parent_id => nil})) %> + + + \ No newline at end of file diff --git a/app/views/layouts/site_editor.html.erb b/app/views/layouts/site_editor.html.erb index 158c9f80..da6efd26 100644 --- a/app/views/layouts/site_editor.html.erb +++ b/app/views/layouts/site_editor.html.erb @@ -23,9 +23,9 @@
- +
diff --git a/config/routes.rb b/config/routes.rb index 863d17fd..63d70d30 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -49,6 +49,7 @@ Orbit::Application.routes.draw do get 'add_attribute_field' end resources :items + match 'update_position' => 'items#update_position' resources :links do member do get 'delete'