Implementing hierarchical editor

--HG--
branch : 1.x
extra : rebase_source : 03ee4afe2e9bb66dff80d8a750609ec537956151
This commit is contained in:
Sebastien Ros 2012-05-14 13:58:47 -07:00
parent 0e2a17d417
commit 35424a4b56
7 changed files with 601 additions and 33 deletions

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appSettings>
<add key="webpages:Enabled" value="false" />
</appSettings>
<system.web>
<httpHandlers>
<!-- iis6 - for any request in this location, return via managed static file handler -->
<add path="*" verb="*" type="System.Web.StaticFileHandler" />
</httpHandlers>
</system.web>
<system.webServer>
<staticContent>
<clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="7.00:00:00" />
</staticContent>
<handlers accessPolicy="Script,Read">
<!--
iis7 - for any request to a file exists on disk, return it via native http module.
accessPolicy 'Script' is to allow for a managed 404 page.
-->
<add name="StaticFile" path="*" verb="*" modules="StaticFileModule" preCondition="integratedMode" resourceType="File" requireAccess="Read" />
</handlers>
</system.webServer>
</configuration>

View File

@ -0,0 +1,400 @@
/*
* jQuery UI Nested Sortable
* v 1.4 / 30 dec 2011
* http://mjsarfatti.com/code/nestedSortable
*
* Depends on:
* jquery.ui.sortable.js 1.8+
*
* Copyright (c) 2010-2012 Manuele J Sarfatti
* Licensed under the MIT License
* http://www.opensource.org/licenses/mit-license.php
*/
(function ($) {
$.widget("mjs.nestedSortable", $.extend({}, $.ui.sortable.prototype, {
options: {
tabSize: 20,
disableNesting: 'mjs-nestedSortable-no-nesting',
errorClass: 'mjs-nestedSortable-error',
listType: 'ol',
maxLevels: 0,
protectRoot: false,
rootID: null,
rtl: false,
isAllowed: function (item, parent) { return true; }
},
_create: function () {
this.element.data('sortable', this.element.data('nestedSortable'));
if (!this.element.is(this.options.listType))
throw new Error('nestedSortable: Please check the listType option is set to your actual list type');
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;
}
}
}
var 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 &&
(o.rtl && (this.positionAbs.left + this.helper.outerWidth() > parentItem.offset().left + parentItem.outerWidth()) ||
!o.rtl && (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 &&
(o.rtl && (this.positionAbs.left + this.helper.outerWidth() < previousItem.offset().left + previousItem.outerWidth() - o.tabSize) ||
!o.rtl && (this.positionAbs.left > previousItem.offset().left + o.tabSize))) {
this._isAllowed(previousItem, level, 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, 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.domPosition.prev) {
$(this.domPosition.prev).after(this.placeholder);
} else {
$(this.domPosition.parent).prepend(this.placeholder);
}
this._trigger("revert", 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 (options) {
var o = $.extend({}, this.options, options),
items = this._getItemsAsjQuery(o && o.connected),
str = [];
$(items).each(function () {
var res = ($(o.item || this).attr(o.attribute || 'id') || '')
.match(o.expression || (/(.+)[-=_](.+)/)),
pid = ($(o.item || this).parent(o.listType)
.parent(o.items)
.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]) : o.rootID));
}
});
if (!str.length && o.key) {
str.push(o.key + '=');
}
return str.join('&');
},
toHierarchy: function (options) {
var o = $.extend({}, this.options, options),
sDepth = o.startDepthCount || 0,
ret = [];
$(this.element).children(o.items).each(function () {
var level = _recursiveItems(this);
ret.push(level);
});
return ret;
function _recursiveItems(item) {
var id = ($(item).attr(o.attribute || 'id') || '').match(o.expression || (/(.+)[-=_](.+)/));
if (id) {
var currentItem = { "id": id[2] };
if ($(item).children(o.listType).children(o.items).length > 0) {
currentItem.children = [];
$(item).children(o.listType).children(o.items).each(function () {
var level = _recursiveItems(this);
currentItem.children.push(level);
});
}
return currentItem;
}
}
},
toArray: function (options) {
var o = $.extend({}, this.options, options),
sDepth = o.startDepthCount || 0,
ret = [],
left = 2;
ret.push({
"item_id": o.rootID,
"parent_id": 'none',
"depth": sDepth,
"left": '1',
"right": ($(o.items, this.element).length + 1) * 2
});
$(this.element).children(o.items).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(o.items).length > 0) {
depth++;
$(item).children(o.listType).children(o.items).each(function () {
right = _recursiveArray($(this), depth, right);
});
depth--;
}
id = ($(item).attr(o.attribute || 'id')).match(o.expression || (/(.+)[-=_](.+)/));
if (depth === sDepth + 1) {
pid = o.rootID;
} else {
var parentItem = ($(item).parent(o.listType)
.parent(o.items)
.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, level, levels) {
var o = this.options,
isRoot = $(this.domPosition.parent).hasClass('ui-sortable') ? true : false;
// Is the root protected?
// Are we trying to nest under a no-nest?
// Are we nesting too deep?
if (!o.isAllowed(parentItem, this.placeholder) ||
parentItem && parentItem.hasClass(o.disableNesting) ||
o.protectRoot && (parentItem == null && !isRoot || isRoot && level > 1)) {
this.placeholder.addClass(o.errorClass);
if (o.maxLevels < levels && o.maxLevels != 0) {
this.beyondMaxLevels = levels - o.maxLevels;
} else {
this.beyondMaxLevels = 1;
}
} else {
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;
}
}
}
}));
$.mjs.nestedSortable.prototype.options = $.extend({}, $.ui.sortable.prototype.options, $.mjs.nestedSortable.prototype.options);
})(jQuery);

View File

@ -0,0 +1,44 @@

(function ($) {
var populate = function (el, prefix) {
var pos = 1;
// direct children
var children = $(el).children('li').each(function (i, child) {
if (!prefix) prefix = '';
child = $(child);
// apply positions to all siblings
child.find('.navigation-position > input').attr('value', prefix + pos);
// recurse position for children
child.children('ol').each(function (i, item) { populate(item, prefix + pos.toString() + '.') });
pos++;
});
};
$('.navigation-menu > ol').nestedSortable({
disableNesting: 'no-nest',
forcePlaceholderSize: true,
handle: 'div',
helper: 'clone',
items: 'li',
maxLevels: 6,
opacity: .6,
placeholder: 'navigation-placeholder',
revert: 50,
tabSize: 30,
tolerance: 'pointer',
toleranceElement: '> div',
stop: function (event, ui) {
// update all positions whenever a menu item was moved
populate(this, '');
}
});
})(jQuery);

Binary file not shown.

After

Width:  |  Height:  |  Size: 897 B

View File

@ -0,0 +1,57 @@
.navigation-menu ol ol {
margin: 0;
padding: 0;
padding-left: 30px; /* gap between each level */
}
.navigation-menu ol.sortable, .navigation-menu ol.sortable ol {
margin: 0;
padding: 0;
list-style-type: none;
}
.navigation-menu ol.sortable {
}
.navigation-menu li {
margin: 0;
padding: 0;
}
.navigation-menu li div {
border: 1px solid #ccc;
cursor: move;
padding: 3px 10px 3px 30px;
margin: 2px;
background: #eee;
display: inline-block;
}
.navigation-menu li div:hover {
background: #eee url(images/move.gif) no-repeat 10px 10px;
}
.navigation-text
{
}
.navigation-position
{
display: none;
}
.navigation-url
{
}
.navigation-actions
{
}
/* style for the dynamically created placeholder */
.navigation-menu li.navigation-placeholder
{
background: #dadada;
}

View File

@ -3,40 +3,22 @@
@using Orchard.Core.Navigation.Models;
@using Orchard.Core.Navigation.ViewModels;
@{ Layout.Title = T("Navigation").ToString(); }
@{
Layout.Title = T("Navigation").ToString();
Style.Include("navigation-admin.css");
Script.Require("jQueryUI_Sortable");
Script.Include("jquery.mjs.nestedSortable.js");
Script.Include("navigation-admin.js");
}
@using (Html.BeginFormAntiForgeryPost()) {
<table class="items">
<colgroup>
<col id="Text" />
<col id="Position" />
<col id="Url" />
<col id="Actions" />
</colgroup>
<thead>
<tr>
<td scope="col">@T("Text")</td>
<td scope="col">@T("Position")</td>
<td scope="col">@T("Url")</td>
<td scope="col"></td>
</tr>
</thead>
<tbody>@{
var menuPartEntryIndex = 0;
foreach (var menuPartEntry in Model.MenuItemEntries) {
var i = menuPartEntryIndex;
<tr>
<td><input type="text" class="text-box" name="@Html.NameOf(m => m.MenuItemEntries[i].Text)" value="@menuPartEntry.Text" /></td>
<td><input type="text" class="text-box" name="@Html.NameOf(m => m.MenuItemEntries[i].Position)" value="@menuPartEntry.Position" /></td>
<td>@if (!menuPartEntry.IsMenuItem) {<input type="text" class="text-box disabled" disabled="disabled" value="@menuPartEntry.Url" /> } else {<input type="text" class="text-box" name="@Html.NameOf(m => m.MenuItemEntries[i].Url)" value="@menuPartEntry.Url" />}</td>
<td><input type="hidden" name="@Html.NameOf(m => m.MenuItemEntries[i].MenuItemId)" value="@menuPartEntry.MenuItemId" />@Html.ActionLink(T("Delete").Text, "Delete", new { id = menuPartEntry.MenuItemId }, new { @class="remove", itemprop="RemoveUrl UnsafeUrl" })</td>
</tr>
++menuPartEntryIndex;
}
}
</tbody>
</table>
<fieldset class="actions"><button type="submit" class="button">@T("Update All")</button></fieldset>
<div class="navigation-menu">
@RenderMenuItems(Model.MenuItemEntries, 0)
</div>
<fieldset class="actions"><button type="submit" class="button">@T("Save All")</button></fieldset>
}
<h2>@T("Add New Item")</h2>
@ -66,4 +48,56 @@
</tr>
</tbody>
</table>
}
}
@helper RenderMenuItems(IList<MenuItemEntry> menuItems, int index) {
@:<ol>
// store current level to detect lowerb or upper level
int currentLevel = Model.MenuItemEntries[index].Position.Split('.').Length - 1;
bool first = true;
for (int i = index; i < Model.MenuItemEntries.Count; i++) {
var menuPartEntry = Model.MenuItemEntries[i];
var level = menuPartEntry.Position.Split('.').Length - 1; // 0 is for root level
if (level > currentLevel) {
// render sub level, then continue to next element
@RenderMenuItems(menuItems, i)
var j = i;
while (j < Model.MenuItemEntries.Count && Model.MenuItemEntries[j].Position.Split('.').Length - 1 > currentLevel) { j++; };
i = j - 1;
continue;
}
if (level == currentLevel) {
if(!first) {
@:</li>
}
first = false;
@:<li class="navigation-menu-item" data-index="@i">
<div>
<span class="navigation-text"><input type="text" class="text-box" name="@Html.NameOf(m => m.MenuItemEntries[i].Text)" value="@menuPartEntry.Text" /></span>
<span class="navigation-position"><input type="text" class="text-box" name="@Html.NameOf(m => m.MenuItemEntries[i].Position)" value="@menuPartEntry.Position" /></span>
<span class="navigation-url">@if (!menuPartEntry.IsMenuItem) {<input type="text textMedium" class="text-box disabled" disabled="disabled" value="@menuPartEntry.Url" /> } else {<input type="text" class="text-box textMedium" name="@Html.NameOf(m => m.MenuItemEntries[i].Url)" value="@menuPartEntry.Url" />}</span>
<span class="navigation-actions"><input type="hidden" name="@Html.NameOf(m => m.MenuItemEntries[i].MenuItemId)" value="@menuPartEntry.MenuItemId" />@Html.ActionLink(T("Delete").Text, "Delete", new { id = menuPartEntry.MenuItemId }, new { @class = "remove", itemprop = "RemoveUrl UnsafeUrl" })</span>
</div>
continue;
}
// done with current level
if (level < currentLevel) {
@:</li>
break;
}
}
@:</ol>
}

View File

@ -277,6 +277,9 @@
<Content Include="Common\Views\Body.Editor.cshtml" />
<Content Include="Contents\Module.txt" />
<Content Include="Contents\Views\Admin\CreatableTypeList.cshtml" />
<Content Include="Navigation\Scripts\jquery.mjs.nestedSortable.js" />
<Content Include="Navigation\Scripts\navigation-admin.js" />
<Content Include="Navigation\Styles\navigation-admin.css" />
<Content Include="Navigation\Styles\images\menu.navigation.png" />
<Content Include="Navigation\Styles\menu.navigation-admin.css" />
<Content Include="Reports\Module.txt" />
@ -483,6 +486,11 @@
<ItemGroup>
<Content Include="Common\Views\Body-Small.Editor.cshtml" />
</ItemGroup>
<ItemGroup>
<Content Include="Navigation\Scripts\Web.config">
<SubType>Designer</SubType>
</Content>
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v10.0\WebApplications\Microsoft.WebApplication.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.