Migrating some existing Routable aspect functionality to Core.Routable module

Exposing user interface for Title and Slug
Calculating Path based on Slug (and container's Path, if present)
Migrating some of the slugify action and jquery support to Core.Routable module
Making a driver for CommonAspect, adding rough placeholder textbox to input container id

--HG--
branch : dev
This commit is contained in:
Louis DeJardin
2010-06-09 15:18:12 -07:00
parent 519fc0cceb
commit 75449c1abe
14 changed files with 502 additions and 78 deletions

View File

@@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Orchard.ContentManagement;
using Orchard.ContentManagement.Drivers;
using Orchard.Core.Common.Models;
using Orchard.Core.Common.ViewModels;
using Orchard.Localization;
using Orchard.Security;
using Orchard.Services;
namespace Orchard.Core.Common.Drivers {
public class CommonDriver : ContentPartDriver<CommonAspect> {
private readonly IContentManager _contentManager;
private readonly IAuthenticationService _authenticationService;
private readonly IAuthorizationService _authorizationService;
private readonly IMembershipService _membershipService;
private readonly IClock _clock;
public CommonDriver(
IContentManager contentManager,
IAuthenticationService authenticationService,
IAuthorizationService authorizationService,
IMembershipService membershipService,
IClock clock) {
_contentManager = contentManager;
_authenticationService = authenticationService;
_authorizationService = authorizationService;
_membershipService = membershipService;
_clock = clock;
T = NullLocalizer.Instance;
}
public Localizer T { get; set; }
protected override DriverResult Editor(CommonAspect part) {
return Combined(OwnerEditor(part, null), ContainerEditor(part, null));
}
protected override DriverResult Editor(CommonAspect instance, ContentManagement.IUpdateModel updater) {
// this event is hooked so the modified timestamp is changed when an edit-post occurs.
instance.ModifiedUtc = _clock.UtcNow;
instance.VersionModifiedUtc = _clock.UtcNow;
return Combined(OwnerEditor(instance, updater), ContainerEditor(instance, updater));
}
DriverResult OwnerEditor(CommonAspect part, IUpdateModel updater) {
var currentUser = _authenticationService.GetAuthenticatedUser();
if (!_authorizationService.TryCheckAccess(Permissions.ChangeOwner, currentUser, part)) {
return null;
}
var model = new OwnerEditorViewModel();
if (part.Owner != null)
model.Owner = part.Owner.UserName;
if (updater != null) {
var priorOwner = model.Owner;
updater.TryUpdateModel(model, "CommonAspect", null, null);
if (model.Owner != null && model.Owner != priorOwner) {
var newOwner = _membershipService.GetUser(model.Owner);
if (newOwner == null) {
updater.AddModelError("CommonAspect.Owner", T("Invalid user name"));
}
else {
part.Owner = newOwner;
}
}
}
return ContentPartTemplate(model, "Parts/Common.Owner", "CommonAspect").Location("primary", "10");
}
DriverResult ContainerEditor(CommonAspect part, IUpdateModel updater) {
var currentUser = _authenticationService.GetAuthenticatedUser();
if (!_authorizationService.TryCheckAccess(Permissions.ChangeOwner, currentUser, part)) {
return null;
}
var model = new ContainerEditorViewModel();
if (part.Container != null)
model.ContainerId = part.Container.ContentItem.Id;
if (updater != null) {
var priorContainerId = model.ContainerId;
updater.TryUpdateModel(model, "CommonAspect", null, null);
if (model.ContainerId != null && model.ContainerId != priorContainerId) {
var newContainer = _contentManager.Get((int)model.ContainerId, VersionOptions.Latest);
if (newContainer == null) {
updater.AddModelError("CommonAspect.ContainerId", T("Invalid container"));
}
else {
part.Container = newContainer;
}
}
}
return ContentPartTemplate(model, "Parts/Common.Container", "CommonAspect").Location("primary", "10.1");
}
}
}

View File

@@ -52,8 +52,8 @@ namespace Orchard.Core.Common.Handlers {
OnPublishing<ContentPart<CommonVersionRecord>>(AssignPublishingDates);
//OnGetDisplayViewModel<CommonAspect>();
OnGetEditorViewModel<CommonAspect>(GetEditor);
OnUpdateEditorViewModel<CommonAspect>(UpdateEditor);
//OnGetEditorViewModel<CommonAspect>(GetEditor);
//OnUpdateEditorViewModel<CommonAspect>(UpdateEditor);
OnIndexing<CommonAspect>((context, commonAspect) => context.IndexDocument
.Add("type", commonAspect.ContentItem.ContentType).Analyze(false)
@@ -157,48 +157,48 @@ namespace Orchard.Core.Common.Handlers {
}
private void GetEditor(BuildEditorModelContext context, CommonAspect instance) {
var currentUser = _authenticationService.GetAuthenticatedUser();
if (!_authorizationService.TryCheckAccess(Permissions.ChangeOwner, currentUser, instance)) {
return;
}
var viewModel = new OwnerEditorViewModel();
if (instance.Owner != null)
viewModel.Owner = instance.Owner.UserName;
//private void GetEditor(BuildEditorModelContext context, CommonAspect instance) {
// var currentUser = _authenticationService.GetAuthenticatedUser();
// if (!_authorizationService.TryCheckAccess(Permissions.ChangeOwner, currentUser, instance)) {
// return;
// }
// var viewModel = new OwnerEditorViewModel();
// if (instance.Owner != null)
// viewModel.Owner = instance.Owner.UserName;
context.AddEditor(new TemplateViewModel(viewModel, "CommonAspect") { TemplateName = "Parts/Common.Owner", ZoneName = "primary", Position = "999" });
}
// context.AddEditor(new TemplateViewModel(viewModel, "CommonAspect") { TemplateName = "Parts/Common.Owner", ZoneName = "primary", Position = "999" });
//}
private void UpdateEditor(UpdateEditorModelContext context, CommonAspect instance) {
// this event is hooked so the modified timestamp is changed when an edit-post occurs.
// kind of a loose rule of thumb. may not be sufficient
instance.ModifiedUtc = _clock.UtcNow;
instance.VersionModifiedUtc = _clock.UtcNow;
//private void UpdateEditor(UpdateEditorModelContext context, CommonAspect instance) {
// // this event is hooked so the modified timestamp is changed when an edit-post occurs.
// // kind of a loose rule of thumb. may not be sufficient
// instance.ModifiedUtc = _clock.UtcNow;
// instance.VersionModifiedUtc = _clock.UtcNow;
var currentUser = _authenticationService.GetAuthenticatedUser();
if (!_authorizationService.TryCheckAccess(Permissions.ChangeOwner, currentUser, instance)) {
return;
}
// var currentUser = _authenticationService.GetAuthenticatedUser();
// if (!_authorizationService.TryCheckAccess(Permissions.ChangeOwner, currentUser, instance)) {
// return;
// }
var viewModel = new OwnerEditorViewModel();
if (instance.Owner != null)
viewModel.Owner = instance.Owner.UserName;
// var viewModel = new OwnerEditorViewModel();
// if (instance.Owner != null)
// viewModel.Owner = instance.Owner.UserName;
var priorOwner = viewModel.Owner;
context.Updater.TryUpdateModel(viewModel, "CommonAspect", null, null);
// var priorOwner = viewModel.Owner;
// context.Updater.TryUpdateModel(viewModel, "CommonAspect", null, null);
if (viewModel.Owner != null && viewModel.Owner != priorOwner) {
var newOwner = _membershipService.GetUser(viewModel.Owner);
if (newOwner == null) {
context.Updater.AddModelError("CommonAspect.Owner", T("Invalid user name"));
}
else {
instance.Owner = newOwner;
}
}
// if (viewModel.Owner != null && viewModel.Owner != priorOwner) {
// var newOwner = _membershipService.GetUser(viewModel.Owner);
// if (newOwner == null) {
// context.Updater.AddModelError("CommonAspect.Owner", T("Invalid user name"));
// }
// else {
// instance.Owner = newOwner;
// }
// }
context.AddEditor(new TemplateViewModel(viewModel, "CommonAspect") { TemplateName = "Parts/Common.Owner", ZoneName = "primary", Position = "999" });
}
// context.AddEditor(new TemplateViewModel(viewModel, "CommonAspect") { TemplateName = "Parts/Common.Owner", ZoneName = "primary", Position = "999" });
//}
}
}

View File

@@ -0,0 +1,8 @@
using System.ComponentModel.DataAnnotations;
namespace Orchard.Core.Common.ViewModels {
public class ContainerEditorViewModel {
public int? ContainerId { get; set; }
}
}

View File

@@ -0,0 +1,7 @@
<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl<ContainerEditorViewModel>" %>
<%@ Import Namespace="Orchard.Core.Common.ViewModels" %>
<fieldset>
<%=Html.LabelFor(m=>m.ContainerId) %>
<%=Html.EditorFor(m=>m.ContainerId) %>
<%=Html.ValidationMessageFor(m=>m.ContainerId) %>
</fieldset>

View File

@@ -66,9 +66,11 @@
</ItemGroup>
<ItemGroup>
<Compile Include="Common\Drivers\BodyDriver.cs" />
<Compile Include="Common\Drivers\CommonDriver.cs" />
<Compile Include="Common\Drivers\RoutableDriver.cs" />
<Compile Include="Common\Controllers\RoutableController.cs" />
<Compile Include="Common\Handlers\RoutableAspectHandler.cs" />
<Compile Include="Common\ViewModels\ContainerEditorViewModel.cs" />
<Compile Include="Contents\Controllers\ItemController.cs" />
<Compile Include="Contents\Handlers\ContentsModuleHandler.cs" />
<Compile Include="Localization\Drivers\LocalizedDriver.cs" />
@@ -155,8 +157,11 @@
<Compile Include="Navigation\ViewModels\NavigationManagementViewModel.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Routable\Routes.cs" />
<Compile Include="Routable\Services\IRoutableService.cs" />
<Compile Include="Routable\Services\RoutablePathConstraint.cs" />
<Compile Include="Routable\Services\RoutablePathConstraintUpdator.cs" />
<Compile Include="Routable\Services\RoutableService.cs" />
<Compile Include="Routable\ViewModels\RoutableEditorViewModel.cs" />
<Compile Include="Routable\ViewModels\RoutableDisplayViewModel.cs" />
<Compile Include="Scheduling\Models\ScheduledTaskRecord.cs" />
<Compile Include="Scheduling\Services\PublishingTaskHandler.cs" />
@@ -201,6 +206,7 @@
</ItemGroup>
<ItemGroup>
<Content Include="Common\Module.txt" />
<Content Include="Common\Views\EditorTemplates\Parts\Common.Container.ascx" />
<Content Include="Contents\Module.txt" />
<Content Include="Contents\Views\Admin\Types.aspx" />
<Content Include="Contents\Views\Admin\List.aspx" />
@@ -214,6 +220,8 @@
<Content Include="Indexing\Module.txt" />
<Content Include="Localization\Module.txt" />
<Content Include="Routable\Module.txt" />
<Content Include="Routable\Scripts\jquery.slugify.js" />
<Content Include="Routable\Views\EditorTemplates\Parts\Routable.IsRoutable.ascx" />
<Content Include="Routable\Views\Item\Display.aspx" />
<Content Include="Settings\Module.txt" />
<Content Include="Settings\Views\Admin\Index.ascx" />

View File

@@ -6,16 +6,20 @@ using Orchard.ContentManagement.Aspects;
using Orchard.Core.Common.Models;
using Orchard.Core.Routable.Models;
using Orchard.Core.Routable.ViewModels;
using Orchard.Data;
using Orchard.Localization;
using Orchard.Mvc.ViewModels;
namespace Orchard.Core.Routable.Controllers {
[ValidateInput(false)]
public class ItemController : Controller {
public class ItemController : Controller, IUpdateModel {
private readonly IContentManager _contentManager;
private readonly ITransactionManager _transactionManager;
private readonly IRoutablePathConstraint _routablePathConstraint;
public ItemController(IContentManager contentManager, IRoutablePathConstraint routablePathConstraint) {
public ItemController(IContentManager contentManager, ITransactionManager transactionManager, IRoutablePathConstraint routablePathConstraint) {
_contentManager = contentManager;
_transactionManager = transactionManager;
_routablePathConstraint = routablePathConstraint;
}
@@ -47,5 +51,39 @@ namespace Orchard.Core.Routable.Controllers {
itemViewModel.TemplateName = "Items/Contents.Item";
}
}
public ActionResult Slugify(string contentType, int? id, int? containerId) {
const string slug = "";
ContentItem contentItem = null;
if (string.IsNullOrEmpty(contentType))
return Json(slug);
if (id != null)
contentItem = _contentManager.Get((int)id, VersionOptions.Latest);
if (contentItem == null) {
contentItem = _contentManager.New(contentType);
if (containerId != null) {
var containerItem = _contentManager.Get((int)containerId);
contentItem.As<ICommonAspect>().Container = containerItem;
}
}
_contentManager.UpdateEditorModel(contentItem, this);
_transactionManager.Cancel();
return Json(contentItem.As<IRoutableAspect>().Slug ?? slug);
}
bool IUpdateModel.TryUpdateModel<TModel>(TModel model, string prefix, string[] includeProperties, string[] excludeProperties) {
return TryUpdateModel(model, prefix, includeProperties, excludeProperties);
}
void IUpdateModel.AddModelError(string key, LocalizedString errorMessage) {
ModelState.AddModelError(key, errorMessage.ToString());
}
}
}

View File

@@ -1,61 +1,99 @@
using JetBrains.Annotations;
using Orchard.ContentManagement;
using Orchard.ContentManagement.Aspects;
using Orchard.ContentManagement.Drivers;
using Orchard.Core.Common.Models;
using Orchard.Core.Common.ViewModels;
using Orchard.Core.Common.Services;
using Orchard.Core.Routable.Models;
using Orchard.Core.Routable.Services;
using Orchard.Core.Routable.ViewModels;
using Orchard.Localization;
using Orchard.UI.Notify;
namespace Orchard.Core.Routable.Drivers {
public class RoutableDriver : ContentPartDriver<IsRoutable> {
protected override DriverResult Editor(IsRoutable part, IUpdateModel updater) {
part.Record.Title = "Routable #" + part.ContentItem.Id;
part.Record.Slug = "routable" + part.ContentItem.Id;
part.Record.Path = "routable" + part.ContentItem.Id;
return base.Editor(part, updater);
private readonly IOrchardServices _services;
private readonly IRoutableService _routableService;
public RoutableDriver(IOrchardServices services, IRoutableService routableService) {
_services = services;
_routableService = routableService;
T = NullLocalizer.Instance;
}
//private const string TemplateName = "Parts/Common.Routable";
//private readonly IOrchardServices _services;
//private readonly IRoutableService _routableService;
//public Localizer T { get; set; }
private const string TemplateName = "Parts/Routable.IsRoutable";
//protected override string Prefix {
// get { return "Routable"; }
//}
public Localizer T { get; set; }
//public Routable(IOrchardServices services, IRoutableService routableService)
//{
// _services = services;
// _routableService = routableService;
protected override string Prefix {
get { return "Routable"; }
}
// T = NullLocalizer.Instance;
//}
int? GetContainerId(IContent item) {
var commonAspect = item.As<ICommonAspect>();
if (commonAspect != null && commonAspect.Container != null) {
return commonAspect.Container.ContentItem.Id;
}
return null;
}
//protected override DriverResult Editor(RoutableAspect part) {
// var model = new RoutableEditorViewModel { Prefix = Prefix, RoutableAspect = part };
// return ContentPartTemplate(model, TemplateName, Prefix).Location("primary", "before.5");
//}
string GetContainerSlug(IContent item) {
var commonAspect = item.As<ICommonAspect>();
if (commonAspect != null && commonAspect.Container != null) {
var routable = commonAspect.Container.As<IRoutableAspect>();
if (routable != null) {
return routable.Slug;
}
}
return null;
}
//protected override DriverResult Editor(RoutableAspect part, IUpdateModel updater) {
// var model = new RoutableEditorViewModel { Prefix = Prefix, RoutableAspect = part };
// updater.TryUpdateModel(model, Prefix, null, null);
protected override DriverResult Editor(IsRoutable part) {
var model = new RoutableEditorViewModel {
ContentType = part.ContentItem.ContentType,
Id = part.ContentItem.Id,
Slug = part.Slug,
Title = part.Title,
ContainerId = GetContainerId(part),
};
// if (!_routableService.IsSlugValid(part.Slug)){
// updater.AddModelError("Routable.Slug", T("Please do not use any of the following characters in your slugs: \"/\", \":\", \"?\", \"#\", \"[\", \"]\", \"@\", \"!\", \"$\", \"&\", \"'\", \"(\", \")\", \"*\", \"+\", \",\", \";\", \"=\". No spaces are allowed (please use dashes or underscores instead).").ToString());
// }
// TEMP: path format patterns replaces this logic
var path = part.Record.Path;
if (path.EndsWith(part.Slug)) {
model.DisplayLeadingPath = path.Substring(0, path.Length - part.Slug.Length);
}
// string originalSlug = part.Slug;
// if(!_routableService.ProcessSlug(part)) {
// _services.Notifier.Warning(T("Slugs in conflict. \"{0}\" is already set for a previously created {2} so now it has the slug \"{1}\"",
// originalSlug, part.Slug, part.ContentItem.ContentType));
// }
// return ContentPartTemplate(model, TemplateName, Prefix).Location("primary", "before.5");
//}
return ContentPartTemplate(model, TemplateName, Prefix).Location("primary", "before.5");
}
protected override DriverResult Editor(IsRoutable part, IUpdateModel updater) {
var model = new RoutableEditorViewModel();
updater.TryUpdateModel(model, Prefix, null, null);
part.Title = model.Title;
part.Slug = model.Slug;
// TEMP: path format patterns replaces this logic
var containerSlug = GetContainerSlug(part);
if (string.IsNullOrEmpty(containerSlug)) {
part.Record.Path = model.Slug;
}
else {
part.Record.Path = containerSlug + "/" + model.Slug;
}
if (!_routableService.IsSlugValid(part.Slug)) {
updater.AddModelError("Routable.Slug", T("Please do not use any of the following characters in your slugs: \"/\", \":\", \"?\", \"#\", \"[\", \"]\", \"@\", \"!\", \"$\", \"&\", \"'\", \"(\", \")\", \"*\", \"+\", \",\", \";\", \"=\". No spaces are allowed (please use dashes or underscores instead).").ToString());
}
string originalSlug = part.Slug;
if (!_routableService.ProcessSlug(part)) {
_services.Notifier.Warning(T("Slugs in conflict. \"{0}\" is already set for a previously created {2} so now it has the slug \"{1}\"",
originalSlug, part.Slug, part.ContentItem.ContentType));
}
return Editor(part);
}
}
}

View File

@@ -0,0 +1,24 @@
jQuery.fn.extend({
slugify: function(options) {
//todo: (heskew) need messaging system
if (!options.target || !options.url)
return;
var args = {
"contentType": options.contentType,
"id": options.id,
"containerId": options.containerId,
__RequestVerificationToken: $("input[name=__RequestVerificationToken]").val()
};
args[$(this).attr("name")] = $(this).val();
jQuery.post(
options.url,
args,
function(data) {
options.target.val(data);
},
"json"
);
}
});

View File

@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using Orchard.Core.Routable.Models;
namespace Orchard.Core.Routable.Services {
public interface IRoutableService : IDependency {
void FillSlug<TModel>(TModel model) where TModel : IsRoutable;
void FillSlug<TModel>(TModel model, Func<string, string> generateSlug) where TModel : IsRoutable;
string GenerateUniqueSlug(string slugCandidate, IEnumerable<string> existingSlugs);
/// <summary>
/// Returns any content item of the specified content type with similar slugs
/// </summary>
IEnumerable<IsRoutable> GetSimilarSlugs(string contentType, string slug);
/// <summary>
/// Validates the given slug
/// </summary>
bool IsSlugValid(string slug);
/// <summary>
/// Defines the slug of a RoutableAspect and validate its unicity
/// </summary>
/// <returns>True if the slug has been created, False if a conflict occured</returns>
bool ProcessSlug(IsRoutable part);
}
}

View File

@@ -0,0 +1,113 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using JetBrains.Annotations;
using Orchard.ContentManagement;
using Orchard.Core.Common.Models;
using Orchard.Core.Routable.Models;
using Orchard.Localization;
using Orchard.UI.Notify;
namespace Orchard.Core.Routable.Services {
[UsedImplicitly]
public class RoutableService : IRoutableService {
private readonly IContentManager _contentManager;
public RoutableService(IContentManager contentManager) {
_contentManager = contentManager;
}
public void FillSlug<TModel>(TModel model) where TModel : IsRoutable {
if (!string.IsNullOrEmpty(model.Slug) || string.IsNullOrEmpty(model.Title))
return;
var slug = model.Title;
var dissallowed = new Regex(@"[/:?#\[\]@!$&'()*+,;=\s]+");
slug = dissallowed.Replace(slug, "-");
slug = slug.Trim('-');
if (slug.Length > 1000)
slug = slug.Substring(0, 1000);
model.Slug = slug.ToLowerInvariant();
}
public void FillSlug<TModel>(TModel model, Func<string, string> generateSlug) where TModel : IsRoutable {
if (!string.IsNullOrEmpty(model.Slug) || string.IsNullOrEmpty(model.Title))
return;
model.Slug = generateSlug(model.Title).ToLowerInvariant();
}
public string GenerateUniqueSlug(string slugCandidate, IEnumerable<string> existingSlugs) {
if (existingSlugs == null || !existingSlugs.Contains(slugCandidate))
return slugCandidate;
int? version = existingSlugs.Select(s => GetSlugVersion(slugCandidate, s)).OrderBy(i => i).LastOrDefault();
return version != null
? string.Format("{0}-{1}", slugCandidate, version)
: slugCandidate;
}
private static int? GetSlugVersion(string slugCandidate, string slug) {
int v;
string[] slugParts = slug.Split(new []{slugCandidate}, StringSplitOptions.RemoveEmptyEntries);
if (slugParts.Length == 0)
return 2;
return int.TryParse(slugParts[0].TrimStart('-'), out v)
? (int?)++v
: null;
}
public IEnumerable<IsRoutable> GetSimilarSlugs(string contentType, string slug)
{
return
_contentManager.Query(contentType).Join<RoutableRecord>()
.List()
.Select(i => i.As<IsRoutable>())
.Where(routable => routable.Slug.StartsWith(slug, StringComparison.OrdinalIgnoreCase)) // todo: for some reason the filter doesn't work within the query, even without StringComparison or StartsWith
.ToArray();
}
public bool IsSlugValid(string slug) {
// see http://tools.ietf.org/html/rfc3987 for prohibited chars
return slug == null || String.IsNullOrEmpty(slug.Trim()) || Regex.IsMatch(slug, @"^[^/:?#\[\]@!$&'()*+,;=\s]+$");
}
public bool ProcessSlug(IsRoutable part)
{
FillSlug(part);
if (string.IsNullOrEmpty(part.Slug))
{
return true;
}
var slugsLikeThis = GetSimilarSlugs(part.ContentItem.ContentType, part.Slug);
// If the part is already a valid content item, don't include it in the list
// of slug to consider for conflict detection
if (part.ContentItem.Id != 0)
slugsLikeThis = slugsLikeThis.Where(p => p.ContentItem.Id != part.ContentItem.Id);
//todo: (heskew) need better messages
if (slugsLikeThis.Count() > 0)
{
var originalSlug = part.Slug;
//todo: (heskew) make auto-uniqueness optional
part.Slug = GenerateUniqueSlug(part.Slug, slugsLikeThis.Select(p => p.Slug));
if (originalSlug != part.Slug) {
return false;
}
}
return true;
}
}
}

View File

@@ -2,7 +2,7 @@
using Orchard.Mvc.ViewModels;
namespace Orchard.Core.Routable.ViewModels {
public class RoutableDisplayViewModel : BaseViewModel {
public class RoutableDisplayViewModel : BaseViewModel {
public ContentItemViewModel<IRoutableAspect> Routable {get;set;}
}
}

View File

@@ -0,0 +1,17 @@
using System;
using System.ComponentModel.DataAnnotations;
namespace Orchard.Core.Routable.ViewModels {
public class RoutableEditorViewModel {
public int Id { get; set; }
public string ContentType { get; set; }
[Required]
public string Title { get; set; }
public string Slug { get; set; }
public int? ContainerId { get; set; }
public string DisplayLeadingPath { get; set; }
}
}

View File

@@ -0,0 +1,31 @@
<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl<Orchard.Core.Routable.ViewModels.RoutableEditorViewModel>" %>
<%@ Import Namespace="Orchard.Utility.Extensions"%>
<%@ Import Namespace="Orchard.ContentManagement.Extenstions"%>
<% Html.RegisterFootScript("jquery.slugify.js"); %>
<fieldset>
<%=Html.LabelFor(m => m.Title) %>
<%=Html.TextBoxFor(m => m.Title, new { @class = "large text" }) %>
</fieldset>
<fieldset class="permalink">
<label class="sub" for="Slug"><%=_Encoded("Permalink")%><br /><span><%=Html.Encode(Request.ToRootUrlString())%>/<%:Model.DisplayLeadingPath %></span></label>
<span><%=Html.TextBoxFor(m => m.Slug, new { @class = "text" })%></span>
</fieldset>
<% using (this.Capture("end-of-page-scripts")) { %>
<script type="text/javascript">
$(function(){
//pull slug input from tab order
$("#<%:Html.FieldIdFor(m=>m.Slug)%>").attr("tabindex",-1);
$("#<%:Html.FieldIdFor(m=>m.Title)%>").blur(function(){
$(this).slugify({
target:$("#<%:Html.FieldIdFor(m=>m.Slug)%>"),
url:"<%=Url.Action("Slugify","Item",new RouteValueDictionary{{"Area","Routable"}})%>",
contentType:"<%=Model.ContentType %>",
id:"<%=Model.Id %>" <%if (Model.ContainerId != null) { %>,
containerId:<%=Model.ContainerId %><%} %>
})
})
})</script>
<% } %>

View File

@@ -23,6 +23,14 @@ namespace Orchard.Mvc.Html {
return Reflect.NameOf(html.ViewData.Model, expression);
}
public static string FieldNameFor<T, TResult>(this HtmlHelper<T> html, Expression<Func<T, TResult>> expression) {
return html.ViewData.TemplateInfo.GetFullHtmlFieldName(ExpressionHelper.GetExpressionText(expression));
}
public static string FieldIdFor<T, TResult>(this HtmlHelper<T> html, Expression<Func<T, TResult>> expression) {
return html.ViewData.TemplateInfo.GetFullHtmlFieldId(ExpressionHelper.GetExpressionText(expression));
}
public static MvcHtmlString SelectOption<T>(this HtmlHelper html, T currentValue, T optionValue, string text) {
return SelectOption(html, optionValue, object.Equals(optionValue, currentValue), text);
}