Updates the placement editor screen to support moving shapes into tabs (#7795)

Fixes #7791
This commit is contained in:
Hazzamanic
2017-08-10 20:14:33 +01:00
committed by Sébastien Ros
parent 7373c11a1e
commit af922a01be
8 changed files with 310 additions and 164 deletions

View File

@@ -28,8 +28,8 @@ namespace Orchard.ContentTypes.Controllers {
private readonly ShellSettings _settings;
public AdminController(
IOrchardServices orchardServices,
IContentDefinitionService contentDefinitionService,
IOrchardServices orchardServices,
IContentDefinitionService contentDefinitionService,
IContentDefinitionManager contentDefinitionManager,
IPlacementService placementService,
Lazy<IEnumerable<IShellSettingsManagerEventHandler>> settingsManagerEventHandlers,
@@ -101,7 +101,7 @@ namespace Orchard.ContentTypes.Controllers {
}
var contentTypeDefinition = _contentDefinitionService.AddType(viewModel.Name, viewModel.DisplayName);
// adds CommonPart by default
_contentDefinitionService.AddPartToType("CommonPart", viewModel.Name);
@@ -148,9 +148,21 @@ namespace Orchard.ContentTypes.Controllers {
if (contentTypeDefinition == null)
return HttpNotFound();
var grouped = _placementService.GetEditorPlacement(id)
.OrderBy(x => x.PlacementInfo.GetPosition(), new FlatPositionComparer())
.ThenBy(x => x.PlacementSettings.ShapeType)
.Where(e => e.PlacementSettings.Zone == "Content")
.GroupBy(x => x.PlacementInfo.GetTab())
.ToDictionary(x => x.Key, y => y.ToList());
var content = grouped.ContainsKey("") ? grouped[""] : new List<DriverResultPlacement>();
var listPlacements = grouped.Values.SelectMany(e => e).ToList();
grouped.Remove("");
var placementModel = new EditPlacementViewModel {
PlacementSettings = contentTypeDefinition.GetPlacement(PlacementType.Editor),
AllPlacements = _placementService.GetEditorPlacement(id).OrderBy(x => x.PlacementSettings.Position, new FlatPositionComparer()).ThenBy(x => x.PlacementSettings.ShapeType).ToList(),
Content = content,
AllPlacements = listPlacements,
Tabs = grouped,
ContentTypeDefinition = contentTypeDefinition,
};
@@ -168,20 +180,10 @@ namespace Orchard.ContentTypes.Controllers {
if (contentTypeDefinition == null)
return HttpNotFound();
var allPlacements = _placementService.GetEditorPlacement(id).ToList();
var result = new List<PlacementSettings>(contentTypeDefinition.GetPlacement(PlacementType.Editor));
contentTypeDefinition.ResetPlacement(PlacementType.Editor);
foreach(var driverPlacement in viewModel.AllPlacements) {
// if the placement has changed, persist it
if (!allPlacements.Any(x => x.PlacementSettings.Equals(driverPlacement.PlacementSettings))) {
result = result.Where(x => !x.IsSameAs(driverPlacement.PlacementSettings)).ToList();
result.Add(driverPlacement.PlacementSettings);
}
}
foreach(var placementSetting in result) {
foreach (var placement in viewModel.AllPlacements) {
var placementSetting = placement.PlacementSettings;
contentTypeDefinition.Placement(PlacementType.Editor,
placementSetting.ShapeType,
placementSetting.Differentiator,
@@ -195,7 +197,7 @@ namespace Orchard.ContentTypes.Controllers {
_settingsManagerEventHandlers.Value.Invoke(x => x.Saved(_settings), Logger);
return RedirectToAction("EditPlacement", new {id});
return RedirectToAction("EditPlacement", new { id });
}
[HttpPost, ActionName("EditPlacement")]
@@ -234,11 +236,11 @@ namespace Orchard.ContentTypes.Controllers {
TryUpdateModel(edited);
typeViewModel.DisplayName = edited.DisplayName ?? string.Empty;
if ( String.IsNullOrWhiteSpace(typeViewModel.DisplayName) ) {
if (String.IsNullOrWhiteSpace(typeViewModel.DisplayName)) {
ModelState.AddModelError("DisplayName", T("The Content Type name can't be empty.").ToString());
}
if ( _contentDefinitionService.GetTypes().Any(t => String.Equals(t.DisplayName.Trim(), typeViewModel.DisplayName.Trim(), StringComparison.OrdinalIgnoreCase) && !String.Equals(t.Name, id)) ) {
if (_contentDefinitionService.GetTypes().Any(t => String.Equals(t.DisplayName.Trim(), typeViewModel.DisplayName.Trim(), StringComparison.OrdinalIgnoreCase) && !String.Equals(t.Name, id))) {
ModelState.AddModelError("DisplayName", T("A type with the same name already exists.").ToString());
}
@@ -271,7 +273,7 @@ namespace Orchard.ContentTypes.Controllers {
_contentDefinitionService.RemoveType(id, true);
Services.Notifier.Success(T("\"{0}\" has been removed.", typeViewModel.DisplayName));
return RedirectToAction("List");
}
@@ -322,7 +324,7 @@ namespace Orchard.ContentTypes.Controllers {
return AddPartsTo(id);
}
return RedirectToAction("Edit", new {id});
return RedirectToAction("Edit", new { id });
}
public ActionResult RemovePartFrom(string id) {
@@ -364,7 +366,7 @@ namespace Orchard.ContentTypes.Controllers {
Services.Notifier.Success(T("The \"{0}\" part has been removed.", viewModel.Name));
return RedirectToAction("Edit", new {id});
return RedirectToAction("Edit", new { id });
}
#endregion
@@ -447,8 +449,7 @@ namespace Orchard.ContentTypes.Controllers {
[HttpPost, ActionName("EditPart")]
[FormValueRequired("submit.Delete")]
public ActionResult DeletePart(string id)
{
public ActionResult DeletePart(string id) {
if (!Services.Authorizer.Authorize(Permissions.EditContentTypes, T("Not allowed to delete a content part.")))
return new HttpUnauthorizedResult();
@@ -497,8 +498,8 @@ namespace Orchard.ContentTypes.Controllers {
if (partViewModel == null) {
// id passed in might be that of a type w/ no implicit field
if (typeViewModel != null) {
partViewModel = new EditPartViewModel {Name = typeViewModel.Name};
_contentDefinitionService.AddPart(new CreatePartViewModel {Name = partViewModel.Name});
partViewModel = new EditPartViewModel { Name = typeViewModel.Name };
_contentDefinitionService.AddPart(new CreatePartViewModel { Name = partViewModel.Name });
_contentDefinitionService.AddPartToType(partViewModel.Name, typeViewModel.Name);
}
else {
@@ -555,7 +556,7 @@ namespace Orchard.ContentTypes.Controllers {
Services.Notifier.Success(T("The \"{0}\" field has been added.", viewModel.DisplayName));
if (typeViewModel != null) {
return RedirectToAction("Edit", new {id});
return RedirectToAction("Edit", new { id });
}
return RedirectToAction("EditPart", new { id });
@@ -573,7 +574,7 @@ namespace Orchard.ContentTypes.Controllers {
var fieldViewModel = partViewModel.Fields.FirstOrDefault(x => x.Name == name);
if(fieldViewModel == null) {
if (fieldViewModel == null) {
return HttpNotFound();
}
@@ -602,14 +603,14 @@ namespace Orchard.ContentTypes.Controllers {
// prevent null reference exception in validation
viewModel.DisplayName = viewModel.DisplayName ?? String.Empty;
// remove extra spaces
viewModel.DisplayName = viewModel.DisplayName.Trim();
if (String.IsNullOrWhiteSpace(viewModel.DisplayName)) {
ModelState.AddModelError("DisplayName", T("The Display Name name can't be empty.").ToString());
}
if (_contentDefinitionService.GetPart(partViewModel.Name).Fields.Any(t => t.Name != viewModel.Name && String.Equals(t.DisplayName.Trim(), viewModel.DisplayName.Trim(), StringComparison.OrdinalIgnoreCase))) {
ModelState.AddModelError("DisplayName", T("A field with the same Display Name already exists.").ToString());
}
@@ -620,7 +621,7 @@ namespace Orchard.ContentTypes.Controllers {
var field = _contentDefinitionManager.GetPartDefinition(id).Fields.FirstOrDefault(x => x.Name == viewModel.Name);
if(field == null) {
if (field == null) {
return HttpNotFound();
}

View File

@@ -147,6 +147,7 @@
<ItemGroup>
<Content Include="Module.txt" />
<Content Include="Scripts\admin-contenttypes.js" />
<Content Include="Scripts\admin-placementeditor.js" />
<Content Include="Styles\Images\menu.contenttypes.png" />
<Content Include="Styles\Images\move.gif" />
<Content Include="Styles\menu.contenttypes-admin.css" />

View File

@@ -4,6 +4,10 @@ namespace Orchard.ContentTypes {
public class ResourceManifest : IResourceManifestProvider {
public void BuildManifests(ResourceManifestBuilder builder) {
builder.Add().DefineStyle("ContentTypesAdmin").SetUrl("orchard-contenttypes-admin.css");
builder.Add().DefineScript("PlacementEditor")
.SetUrl("admin-placementeditor.js")
.SetDependencies("jQueryUI_Sortable");
}
}
}

View File

@@ -0,0 +1,130 @@
(function ($) {
var assignPositions = function () {
var position = 0;
$('.type').each(function () {
var input = $(this);
var tab = input.closest(".zone-container").data("tab");
//input = input.next();
var postab = tab != "" ? position + "#" + tab : position + "";
reAssignIdName(input, position); // type
input = input.next();
reAssignIdName(input, position); // differentiator
input = input.next();
reAssignIdName(input, position); // zone
input = input.next();
reAssignIdName(input, position); // position
input.val(postab);
position++;
});
};
var reAssignIdName = function (input, pos) {
var name = input.attr('name');
input.attr('name', name.replace(new RegExp("\\[.*\\]", 'gi'), '[' + pos + ']'));
var id = input.attr('id');
input.attr('id', id.replace(new RegExp('_.*__', 'i'), '_' + pos + '__'));
};
var startPos;
function initTab() {
$(".tabdrag").sortable({
placeholder: "placement-placeholder",
connectWith: ".tabdrag",
stop: function (event, ui) {
assignPositions();
}
});
}
$('#sortableTabs').sortable({
placeholder: "tab-placeholder",
start: function (event, ui) {
var self = $(ui.item);
startPos = self.prevAll().size();
},
stop: function (event, ui) {
assignPositions();
$('#save-message').show();
}
});
$("#newTab").click(function (e) {
e.preventDefault();
// get the new tab name, cancel if blank
var tab = $("#tabName").val().replace(/\s/g, "");
if (!tab.length) {
return;
}
if (tab.toLowerCase() === "content") {
$("#tabName").val("");
return;
}
// in tabs already
var tabs = getTabs(true);
if ($.inArray(tab, tabs) >= 0) {
$("#tabName").val("");
return;
}
$("#sortableTabs").append('<div data-tab="' + tab + '" class="zone-container tab-container"><h2><a class="delete">Delete</a>'
+ tab + '</h2><ul class="tabdrag"></ul></div>'
);
// make it sortable
initTab();
$("#sortableTabs").sortable("refresh");
// clear the textbox
$("#tabName").val("");
});
// remove tabs
// append items to content, create content if not there
$("#placement").on("click", ".delete", function (e) {
var me = $(this);
var parent = me.parent(".zone-container");
var list = parent.children(".tabdrag").html();
// get first tab
var newList = $("#placement .tabdrag").first();
if (newList.length) {
parent.remove();
newList.append(list);
}
assignPositions();
});
// toggle editor shapes
$("#placement").on("click", ".toggle", function (e) {
var $this = $(this);
var shape = $(this).next().next();
shape.toggle();
if ($this.text() == 'Show Editor Shape') {
$this.text('Hide Editor Shape');
} else {
$this.text('Show Editor Shape');
}
})
// returns all the tabs
function getTabs(header) {
var tabs = [];
$(".zone-container").each(function (index, e) {
tabs.push($(e).data("tab"));
});
return tabs;
}
initTab();
assignPositions();
$('.shape-editor *').attr('disabled', 'disabled');
$("#placement").disableSelection();
})(jQuery);

View File

@@ -18,6 +18,7 @@ using Orchard.UI.Zones;
namespace Orchard.ContentTypes.Services {
public class DriverResultPlacement {
public PlacementInfo PlacementInfo { get; set; }
public PlacementSettings PlacementSettings { get; set; }
public DriverResult ShapeResult { get; set; }
public dynamic Shape { get; set; }
@@ -201,6 +202,7 @@ namespace Orchard.ContentTypes.Services {
yield return new DriverResultPlacement {
PlacementInfo = placement,
Shape = itemShape.Content,
ShapeResult = contentShapeResult,
PlacementSettings = new PlacementSettings {

View File

@@ -21,38 +21,38 @@
.manage-part, .manage-field {
margin-bottom: 0;
padding:0;
padding: 0;
border-bottom: 1px solid #EAEAEA;
}
.manage-part h3,
.manage-field h3 {
display: relative;
line-height: 1.4em;
padding-bottom: 0;
padding-top: 0;
}
.manage-part h3,
.manage-field h3 {
display: relative;
line-height: 1.4em;
padding-bottom: 0;
padding-top: 0;
}
.manage-part h3,
.manage-field h3,
.manage-part h4,
.manage-type .manage-field .details,
.manage-type .manage-part .manage-field,
.manage-type .manage-part .settings {
padding-left: 30px;
padding-right: 30px;
}
.manage-part h3,
.manage-field h3,
.manage-part h4,
.manage-type .manage-field .details,
.manage-type .manage-part .manage-field,
.manage-type .manage-part .settings {
padding-left: 30px;
padding-right: 30px;
}
.manage-part h3, .manage-field h3 {
margin: 1em 0 !important;
padding-left: 30px;
width: 90%;
}
.manage-part h3, .manage-field h3 {
margin: 1em 0 !important;
padding-left: 30px;
width: 90%;
}
.manage-part .expando-glyph, .manage-field .expando-glyph {
width:16px;
height:16px;
}
.manage-part .expando-glyph, .manage-field .expando-glyph {
width: 16px;
height: 16px;
}
.manage-type .manage-field .settings {
padding-bottom: 10px;
@@ -176,38 +176,81 @@ fieldset.action {
/* PLACEMENT EDITOR */
#placement li {
margin-bottom: 10px;
padding: 0px;
border: 1px solid #eee;
.tabdrag {
min-height: 20px;
}
#placement li .shape-type {
cursor: move;
background-color: #eee;
background: #EEE url(images/move.gif) no-repeat 10px 15px;
height: 30px;
padding: 10px 0px 0px 30px;
.zone-container {
border: 1px dotted #6F6F6F;
padding: 10px;
margin-bottom: 10px;
}
.zone-container li {
margin-bottom: 10px;
padding: 0px;
border: 1px solid #eee;
}
#placement li .shape-editor {
padding: 10px;
background: white;
}
#placement .zone-container h2 {
cursor: move;
margin-top: 0px;
padding-left: 20px;
padding-bottom: 15px;
}
#placement .tab-container {
background: white url(images/move.gif) no-repeat 10px 15px;
}
.zone-container li .shape-type {
cursor: move;
background-color: #eee;
background: #EEE url(images/move.gif) no-repeat 10px 10px;
height: 30px;
padding: 5px 0px 0px 30px;
}
#placement #content-tab {
cursor: default;
}
.zone-container li .toggle {
float: right;
padding-top: 5px;
padding-right: 5px;
cursor: pointer;
}
.zone-container li .shape-editor {
padding: 10px;
background: white;
display: none;
}
#save-message {
display: none;
margin-bottom: 10px;
}
#placement fieldset {
.zone-container fieldset {
float: inherit; /* prevent bad layout if float is defined to left in specific parts, e.g. datetimepicker */
height: auto;
}
.tab-placeholder {
background: #FDF5BC;
border: 1px solid #FDF5BC;
height: 100px;
}
.placement-placeholder {
background: #FDF5BC;
border: 1px solid #FDF5BC;
height: 100px;
height: 40px;
}
.zone-container .delete {
float: right;
cursor: pointer;
}

View File

@@ -6,7 +6,8 @@ using Orchard.ContentTypes.Settings;
namespace Orchard.ContentTypes.ViewModels {
public class EditPlacementViewModel {
public ContentTypeDefinition ContentTypeDefinition { get; set; }
public PlacementSettings[] PlacementSettings { get; set; }
public List<DriverResultPlacement> AllPlacements { get; set; }
public Dictionary<string, List<DriverResultPlacement>> Tabs { get; set; }
public List<DriverResultPlacement> Content { get; set; }
}
}

View File

@@ -1,107 +1,71 @@
@using Orchard.ContentTypes.Services
@model Orchard.ContentTypes.ViewModels.EditPlacementViewModel
@{
Style.Require("ContentTypesAdmin");
Script.Require("jQueryUI_Sortable");
Script.Require("PlacementEditor").AtFoot();
Layout.Title = T("Edit Placement - {0}", Model.ContentTypeDefinition.DisplayName).ToString();
var hiddenShapes = Model.AllPlacements.Where(x => String.IsNullOrEmpty(x.PlacementSettings.Zone) && (String.IsNullOrWhiteSpace(x.PlacementSettings.Position) || x.PlacementSettings.Position == "-"));
int i = 0;
}
@helper RenderPlacement(DriverResultPlacement p, int i) {
var placement = p.PlacementSettings;
<li class="place" data-shape-type="@placement.ShapeType" data-shape-differentiator="@placement.Differentiator" data-shape-zone="Content" data-shape-position="@placement.Position">
<span class="toggle">Show Editor Shape</span>
<div class="shape-type"><h3>@placement.ShapeType @placement.Differentiator</h3></div>
<div class="shape-editor">
@try {
@Display(p.Shape)
}
catch {
}
</div>
@Html.HiddenFor(m => m.AllPlacements[i].PlacementSettings.ShapeType, new { @class = "type" })
@Html.HiddenFor(m => m.AllPlacements[i].PlacementSettings.Differentiator, new { @class = "differentiator" })
@Html.HiddenFor(m => m.AllPlacements[i].PlacementSettings.Zone, new { @class = "zone" })
@Html.HiddenFor(m => m.AllPlacements[i].PlacementSettings.Position, new { @class = "position" })
</li>
}
<div id="save-message" class="message message-Warning">@T("You need to hit \"Save\" in order to save your changes.")</div>
@using (Html.BeginFormAntiForgeryPost()) {
@Html.ValidationSummary()
<div id="placement">
<div data-tab="" class="zone-container" id="content-tab">
<h2>Content</h2>
<ul class="tabdrag">
@foreach (var p in Model.Content) {
@RenderPlacement(p, i);
i++;
}
</ul>
</div>
<ul id="placement">
@for (int i = 0; i < Model.AllPlacements.Count; i++ ) {
var placement = Model.AllPlacements[i].PlacementSettings;
if(placement.Zone != "Content") {
continue;
<div id="sortableTabs">
@foreach (var tab in Model.Tabs) {
<div data-tab="@tab.Key" class="zone-container tab-container">
<a class="delete">Delete</a>
<h2>@tab.Key</h2>
<ul class="tabdrag">
@foreach (var p in tab.Value) {
@RenderPlacement(p, i);
i++;
}
</ul>
</div>
}
<li data-shape-type="@placement.ShapeType" data-shape-differentiator="@placement.Differentiator" data-shape-zone="Content" data-shape-position="@placement.Position">
<div class="shape-type"><h3>@placement.ShapeType @placement.Differentiator</h3></div>
<div class="shape-editor">
@try {
@Display(Model.AllPlacements[i].Shape)
}
catch {
}
</div>
@* @shape.Position @(Model.PlacementSettings.Any(x => x.Equals(shape)))*@
@Html.HiddenFor(m => m.AllPlacements[i].PlacementSettings.ShapeType, new { @class = "type" })
@Html.HiddenFor(m => m.AllPlacements[i].PlacementSettings.Differentiator, new { @class = "differentiator" })
@Html.HiddenFor(m => m.AllPlacements[i].PlacementSettings.Zone, new { @class = "zone" })
@Html.HiddenFor(m => m.AllPlacements[i].PlacementSettings.Position, new { @class = "position" })
</li>
}
</ul>
</div>
</div>
<div>
<input type="text" id="tabName" />
<button class="primaryAction" id="newTab">@T("New Tab")</button>
</div>
<fieldset class="action">
<button class="primaryAction" type="submit" name="submit.Save" value="Save">@T("Save")</button>
<button class="primaryAction" type="submit" name="submit.Restore" value="Restore" itemprop="RemoveUrl" data-message="@T("Are you sure you want to restore these placements?")">@T("Restore")</button>
</fieldset>
}
@using (Script.Foot()) {
<script type="text/javascript">
//<![CDATA[
(function ($) {
var assignPositions = function () {
var position = 0;
$('.type').each(function () {
var input = $(this);
reAssignIdName(input, position); // type
input = input.next();
reAssignIdName(input, position); // differentiator
input = input.next();
reAssignIdName(input, position); // zone
input = input.next();
reAssignIdName(input, position); // position
input.val(++position);
});
};
var reAssignIdName = function (input, pos) {
var name = input.attr('name');
input.attr('name', name.replace(new RegExp("\\[.*\\]", 'gi'), '[' + pos + ']'));
var id = input.attr('id');
input.attr('id', id.replace(new RegExp('_.*__', 'i'), '_' + pos + '__'));
};
assignPositions();
var startPos;
$('#placement').sortable({
placeholder: "placement-placeholder",
start: function (event, ui) {
var self = $(ui.item);
startPos = self.prevAll().size();
},
stop: function (event, ui) {
assignPositions();
$('#save-message').show();
}
});
$('.shape-editor *').attr('disabled', 'disabled');
$("#placement").disableSelection();
})(jQuery);
//]]>
</script>
}