Merge pull request #5674 from RoyalVeterinaryCollege/4409

#4409: Use service for autocomplete enabled taxonomy field and maintain ordering
This commit is contained in:
Sébastien Ros
2015-09-28 09:37:29 -07:00
10 changed files with 170 additions and 127 deletions

View File

@@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Web.Http;
using Orchard.ContentManagement;
using Orchard.Core.Title.Models;
using Orchard.Localization;
using Orchard.Logging;
using Orchard.Taxonomies.Helpers;
using Orchard.Taxonomies.Models;
using Orchard.Taxonomies.Services;
using System.Linq;
using Orchard.Security;
using Orchard.Taxonomies.ViewModels;
namespace Orchard.Taxonomies.Controllers {
public class TagsController : ApiController {
private readonly ITaxonomyService _taxonomyService;
private readonly IContentManager _contentManager;
private readonly IAuthorizer _authorizer;
public Localizer T { get; set; }
protected ILogger Logger { get; set; }
public TagsController(
ITaxonomyService taxonomyService,
IContentManager contentManager,
IAuthorizer authorizer) {
_taxonomyService = taxonomyService;
T = NullLocalizer.Instance;
_contentManager = contentManager;
_authorizer = authorizer;
Logger = NullLogger.Instance;
}
public IEnumerable<Tag> Get(int taxonomyId, bool leavesOnly, string query) {
if (!_authorizer.Authorize(StandardPermissions.AccessAdminPanel)) {
throw new UnauthorizedAccessException("Can't access the admin");
}
if (string.IsNullOrEmpty(query)) return new List<Tag>();
var allTerms = leavesOnly
? _taxonomyService.GetTerms(taxonomyId).ToList()
: new List<TermPart>();
var matchingTerms = _contentManager.Query<TermPart, TermPartRecord>()
.Where(t => t.TaxonomyId == taxonomyId)
.Join<TitlePartRecord>()
.Where(r => r.Title.Contains(query))
.List()
.Select(t => BuildTag(t, leavesOnly, allTerms))
.OrderBy(t => t.Label)
.ToList();
return matchingTerms;
}
private static Tag BuildTag(TermPart term, bool leavesOnly, IEnumerable<TermPart> terms) {
return new Tag {
Value = term.Id,
Label = term.Name,
Disabled = !term.Selectable || (leavesOnly && terms.Any(t => t.Path.Contains(term.Path + term.Id))),
Levels = term.GetLevels()
};
}
}
}

View File

@@ -60,7 +60,7 @@ namespace Orchard.Taxonomies.Drivers {
var settings = field.PartFieldDefinition.Settings.GetModel<TaxonomyFieldSettings>();
var appliedTerms = GetAppliedTerms(part, field, VersionOptions.Latest).ToDictionary(t => t.Id, t => t);
var taxonomy = _taxonomyService.GetTaxonomyByName(settings.Taxonomy);
var terms = taxonomy != null
var terms = taxonomy != null && !settings.Autocomplete
? _taxonomyService.GetTerms(taxonomy.Id).Where(t => !string.IsNullOrWhiteSpace(t.Name)).Select(t => t.CreateTermEntry()).ToList()
: new List<TermEntry>(0);
@@ -70,9 +70,11 @@ namespace Orchard.Taxonomies.Drivers {
DisplayName = field.DisplayName,
Name = field.Name,
Terms = terms,
SelectedTerms = appliedTerms.Select(t => t.Value),
Settings = settings,
SingleTermId = terms.Where(t => t.IsChecked).Select(t => t.Id).FirstOrDefault(),
TaxonomyId = taxonomy != null ? taxonomy.Id : 0
SingleTermId = appliedTerms.Select(t => t.Key).FirstOrDefault(),
TaxonomyId = taxonomy != null ? taxonomy.Id : 0,
HasTerms = taxonomy != null && _taxonomyService.GetTermsCount(taxonomy.Id) > 0
};
var templateName = settings.Autocomplete ? "Fields/TaxonomyField.Autocomplete" : "Fields/TaxonomyField";

View File

@@ -50,6 +50,9 @@
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.CSharp" />
<Reference Include="Newtonsoft.Json">
<HintPath>..\..\..\..\lib\newtonsoft.json\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.ComponentModel.DataAnnotations">
<RequiredTargetFramework>3.5</RequiredTargetFramework>
@@ -59,6 +62,10 @@
<Reference Include="System.Web.DynamicData" />
<Reference Include="System.Web.Entity" />
<Reference Include="System.Web.Extensions" />
<Reference Include="System.Web.Http, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\..\..\lib\aspnetwebapi\System.Web.Http.dll</HintPath>
</Reference>
<Reference Include="System.Web.Mvc, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\..\..\lib\aspnetmvc\System.Web.Mvc.dll</HintPath>
@@ -72,6 +79,8 @@
<ItemGroup>
<Compile Include="AdminMenu.cs" />
<Compile Include="Controllers\AdminController.cs" />
<Compile Include="ViewModels\Tag.cs" />
<Compile Include="Controllers\TagsController.cs" />
<Compile Include="Controllers\TermAdminController.cs" />
<Compile Include="Drivers\TaxonomyNavigationPartDriver.cs" />
<Compile Include="Drivers\TermsPartDriver.cs" />

View File

@@ -2,49 +2,24 @@
/* Helper functions
**********************************************************************/
var addTag = function ($plugin, label) {
$plugin.tagit("add", label);
};
var removeTag = function ($plugin, label) {
var tags = $plugin.tagit("tags");
var index = findTagIndexByLabel(tags, label);
if (index == -1)
return;
tags.splice(index, 1);
$plugin.tagit("fill", tags);
};
var findTagIndexByLabel = function (tags, label) {
for (var i = 0; i < tags.length; i++) {
var tag = tags[i];
if (tag.label == label) {
return i;
}
}
return -1;
};
var createTermCheckbox = function ($wrapper, tag) {
var $ul = $("ul.terms", $wrapper);
var singleChoice = $(".terms-editor", $wrapper).data("singlechoice");
var namePrefix = $wrapper.data("name-prefix");
var idPrefix = $wrapper.data("id-prefix");
var nextIndex = $("li", $ul).length;
var id = isNaN(tag.value) ? -nextIndex : tag.value;
var checkboxId = idPrefix + "_Terms_" + nextIndex + "__IsChecked";
var checkboxName = namePrefix + ".Terms[" + nextIndex + "].IsChecked";
var radioName = namePrefix + ".SingleTermId";
var checkboxHtml = "<input type=\"checkbox\" value=\"true\" checked=\"checked\" data-term=\"" + tag + "\" data-term-identity=\"" + tag.toLowerCase() + "\" id=\"" + checkboxId + "\" name=\"" + checkboxName + "\" />";
var radioHtml = "<input type=\"radio\" value=\"" + -nextIndex + "\" checked=\"checked\" data-term=\"" + tag + "\" data-term-identity=\"" + tag.toLowerCase() + "\" id=\"" + checkboxId + "\" name=\"" + radioName + "\" />";
var checkboxHtml = "<input type=\"checkbox\" value=\"true\" checked=\"checked\" data-term=\"" + tag.label + "\" data-term-identity=\"" + tag.label.toLowerCase() + "\" id=\"" + checkboxId + "\" name=\"" + checkboxName + "\" />";
var radioHtml = "<input type=\"radio\" value=\"" + id + "\" checked=\"checked\" data-term=\"" + tag.label + "\" data-term-identity=\"" + tag.label.toLowerCase() + "\" id=\"" + checkboxId + "\" name=\"" + radioName + "\" />";
var inputHtml = singleChoice ? radioHtml : checkboxHtml;
var $li = $("<li>" +
inputHtml +
"<input type=\"hidden\" value=\"" + -nextIndex + "\" id=\"" + idPrefix + "_Terms_" + nextIndex + "__Id" + "\" name=\"" + namePrefix + ".Terms[" + nextIndex + "].Id" + "\" />" +
"<input type=\"hidden\" value=\"" + tag + "\" id=\"" + idPrefix + "_Terms_" + nextIndex + "__Name" + "\" name=\"" + namePrefix + ".Terms[" + nextIndex + "].Name" + "\" />" +
"<label class=\"forcheckbox\" for=\"" + checkboxId + "\">" + tag + "</label>" +
"<input type=\"hidden\" value=\"" + id + "\" id=\"" + idPrefix + "_Terms_" + nextIndex + "__Id" + "\" name=\"" + namePrefix + ".Terms[" + nextIndex + "].Id" + "\" />" +
"<input type=\"hidden\" value=\"" + tag.label + "\" id=\"" + idPrefix + "_Terms_" + nextIndex + "__Name" + "\" name=\"" + namePrefix + ".Terms[" + nextIndex + "].Name" + "\" />" +
"<label class=\"forcheckbox\" for=\"" + checkboxId + "\">" + tag.label + "</label>" +
"</li>").hide();
if (singleChoice) {
@@ -68,67 +43,20 @@
var $tagIt = $("ul.tagit", $wrapper);
var singleChoice = $(".terms-editor", $wrapper).data("singlechoice");
var $terms = $("ul.terms", $wrapper);
var $termCheckbox = $("input[data-term-identity='" + tag.label.toLowerCase() + "']", $terms).filter(function () {
return $(this).siblings("input[value='" + tag.value + "']").length;
});
if ($termCheckbox.is(":disabled")) {
removeTag($tagIt, tagLabelOrValue);
return;
}
if ($termCheckbox.length == 0 && action == "added") {
createTermCheckbox($wrapper, tag.label, this);
}
$termCheckbox.prop("checked", action == "added");
if (singleChoice && action == "added") {
$tagIt.tagit("fill", [tag.label]);
$tagIt.tagit("fill", tag);
}
if (singleChoice) {
if (action == "added") {
var $option = $("select.term-picker", $wrapper).find("option[data-term-identity='" + tag.label.toLowerCase() + "']");
$option.remove();
} else {
$("select.term-picker", $wrapper).append("<option data-term=\"" + tag.label + "\" data-term-identity=\"" + tag.label.toLowerCase() + "\">" + tag.label + "</option>");
}
}
$terms.empty();
$($tagIt.tagit("tags")).each(function (index, tag) {
createTermCheckbox($wrapper, tag, this);
});
$(".no-terms", $wrapper).hide();
};
$("fieldset.taxonomy-wrapper .expando").on("change", "input[data-term]:enabled", function(e) {
var $checkbox = $(this);
var term = $checkbox.data("term");
var $wrapper = $checkbox.parents("fieldset.taxonomy-wrapper:first");
var $tagIt = $("ul.tagit", $wrapper);
var isChecked = $checkbox.is(":checked");
isChecked ? addTag($tagIt, term) : removeTag($tagIt, term);
});
$("select.term-picker").change(function (e) {
var $select = $(this);
var $firstOption = $("option:first", $select);
if ($firstOption.is(":selected"))
return;
var $selecedOption = $("option:selected", $select);
var $wrapper = $select.parents("fieldset.taxonomy-wrapper:first");
var $tagIt = $("ul.tagit", $wrapper);
var term = $selecedOption.text();
var singleChoice = $(".terms-editor", $wrapper).data("singlechoice");
addTag($tagIt, term);
$select.val("");
if (!singleChoice)
$selecedOption.remove();
});
var renderAutocompleteItem = function (ul, item) {
var label = item.label;
@@ -148,20 +76,28 @@
/* Initialization
**********************************************************************/
$(".terms-editor").each(function () {
var allTerms = $(this).data("all-terms");
var selectedTerms = $(this).data("selected-terms");
var $tagit = $("> ul", this).tagit({
tagSource: allTerms,
tagSource: function (request, response) {
var termsEditor = $(this.element).parents(".terms-editor");
$.getJSON("/api/taxonomies/tags", { taxonomyId: termsEditor.data("taxonomy-id"), leavesOnly: termsEditor.data("leaves-only"), query: request.term }, function (data, status, xhr) {
response(data);
});
},
initialTags: selectedTerms,
triggerKeys: ['enter', 'tab'], // default is ['enter', 'space', 'comma', 'tab'] but we remove comma and space to allow them in the terms
allowNewTags: $(this).data("allow-new-terms"),
tagsChanged: onTagsChanged,
caseSensitive: false,
minLength: 0
}).data("uiTagit");
minLength: 0,
sortable: true
}).data("ui-tagit");
$tagit.input.autocomplete().data("uiAutocomplete")._renderItem = renderAutocompleteItem;
$tagit.input.autocomplete().data("ui-autocomplete")._renderItem = renderAutocompleteItem;
$tagit.input.autocomplete().on("autocompletefocus", function (event, ui) {
event.preventDefault();
});
});

View File

@@ -41,6 +41,7 @@ namespace Orchard.Taxonomies.Services {
IEnumerable<TermPart> GetTerms(int taxonomyId);
int GetTermsCount(int taxonomyId);
TermPart GetTerm(int id);
TermPart GetTermByName(int taxonomyId, string name);
void DeleteTerm(TermPart termPart);

View File

@@ -185,6 +185,12 @@ namespace Orchard.Taxonomies.Services {
return TermPart.Sort(result);
}
public int GetTermsCount(int taxonomyId) {
return _contentManager.Query<TermPart, TermPartRecord>()
.Where(x => x.TaxonomyId == taxonomyId)
.Count();
}
public TermPart GetTerm(int id) {
return _contentManager
.Query<TermPart, TermPartRecord>()

View File

@@ -2,6 +2,10 @@
opacity: 0.3;
}
.ui-autocomplete .ui-menu-item {
float: none;
}
.ui-autocomplete .ui-menu-item a {
font-size: 0.8em;
}

View File

@@ -0,0 +1,17 @@
using Newtonsoft.Json;
namespace Orchard.Taxonomies.ViewModels {
public class Tag {
[JsonProperty("label")]
public string Label { get; set; }
[JsonProperty("value")]
public int Value { get; set; }
[JsonProperty("levels")]
public int Levels { get; set; }
[JsonProperty("disabled")]
public bool Disabled { get; set; }
}
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using Orchard.Taxonomies.Models;
using Orchard.Taxonomies.Settings;
namespace Orchard.Taxonomies.ViewModels {
@@ -8,6 +9,8 @@ namespace Orchard.Taxonomies.ViewModels {
public string DisplayName { get; set; }
public TaxonomyFieldSettings Settings { get; set; }
public IList<TermEntry> Terms { get; set; }
public IEnumerable<TermPart> SelectedTerms { get; set; }
public int SingleTermId { get; set; }
public bool HasTerms { get; set; }
}
}

View File

@@ -1,5 +1,6 @@
@model TaxonomyFieldViewModel
@using Orchard.Taxonomies.Helpers;
@using Orchard.Taxonomies.Models
@using Orchard.Utility.Extensions;
@using Orchard.Taxonomies.ViewModels;
@@ -18,60 +19,61 @@
Script.Include("~/Themes/TheAdmin/scripts/admin.js").AtFoot();
Script.Include("admin-taxonomy-expando.js").AtFoot();
var termIndex = 0;
}
@functions {
bool IsTermDisabled(TermEntry term) {
bool IsTermDisabled(TermPart term) {
return !term.Selectable || (Model.Settings.LeavesOnly && Model.Terms.Any(t => t.Path.Contains(term.Path + term.Id)));
}
}
@{
var allTerms = Newtonsoft.Json.JsonConvert.SerializeObject(Model.Terms.Select(x => new { label = x.Name, value = x.Id, levels = x.GetLevels(), disabled = IsTermDisabled(x) }));
var selectedTerms = Newtonsoft.Json.JsonConvert.SerializeObject(Model.Terms.Where(x => x.IsChecked).Select(x => new { label = x.Name, value = x.Id, levels = 0, disabled = true }));
var termIndex = 0;
var checkedTerms = Model.SelectedTerms.ToList();
var selectedTerms = Newtonsoft.Json.JsonConvert.SerializeObject(checkedTerms.Select(x => new { label = x.Name, value = x.Id, levels = 0, disabled = true }));
}
<fieldset class="taxonomy-wrapper" data-name-prefix="@Html.FieldNameFor(m => m)" data-id-prefix="@Html.FieldIdFor(m => m)">
<label @if(Model.Settings.Required) { <text>class="required"</text> }>@Model.DisplayName</label>
@if (Model.Settings.Autocomplete) {
<div class="terms-editor text text-medium" data-all-terms="@allTerms" data-selected-terms="@selectedTerms" data-allow-new-terms="@Model.Settings.AllowCustomTerms.ToString().ToLower()" data-singlechoice="@Model.Settings.SingleChoice.ToString().ToLower()">
<ul></ul>
@if (Model.Settings.SingleChoice) {
<div class="hint">@T("Enter a single term. Hit <i>tab</i> or <i>enter</i> to apply the term.") @if (!Model.Settings.AllowCustomTerms) { <text>@T("This taxonomy does not allow you to create new terms.") </text> }</div>
}
else {
<div class="hint">@T("Enter multiple terms. Hit <i>tab</i>, <i>enter</i> or <i>,</i> to add multiple terms.") @if (!Model.Settings.AllowCustomTerms) { <text>@T("This taxonomy does not allow you to create new terms.") </text> }</div>
}
</div>
<div class="terms-editor text text-medium" data-taxonomy-id="@Model.TaxonomyId" data-leaves-only="@Model.Settings.LeavesOnly" data-selected-terms="@selectedTerms" data-allow-new-terms="@Model.Settings.AllowCustomTerms.ToString().ToLower()" data-singlechoice="@Model.Settings.SingleChoice.ToString().ToLower()">
<ul></ul>
@if (Model.Settings.SingleChoice) {
<div class="hint">@T("Enter a single term. Hit <i>tab</i> or <i>enter</i> to apply the term.") @if (!Model.Settings.AllowCustomTerms) { <text>@T("This taxonomy does not allow you to create new terms.") </text> }</div>
}
else {
<div class="hint">@T("Enter multiple terms. Hit <i>tab</i>, <i>enter</i> or <i>,</i> to add multiple terms.") @if (!Model.Settings.AllowCustomTerms) { <text>@T("This taxonomy does not allow you to create new terms.") </text> }</div>
}
</div>
}
@if (!String.IsNullOrWhiteSpace(Model.Settings.Hint)) {
<span class="hint">@Model.Settings.Hint</span>
<span class="hint">@Model.Settings.Hint</span>
}
<div class="hidden-taxonomy-state">
<ul class="terms">
@foreach (var entry in Model.Terms) {
var ti = termIndex;
<li>
@{
var disabled = IsTermDisabled(entry);
if (Model.Settings.SingleChoice) {
<input @if (disabled) { <text> disabled="disabled" </text> } type="radio" value="@Model.Terms[ti].Id" @if (entry.Id == Model.SingleTermId) { <text> checked="checked" </text> } name="@Html.FieldNameFor(m => m.SingleTermId)" id="@Html.FieldIdFor(m => m.Terms[ti].IsChecked)" data-term="@entry.Name" data-term-identity="@entry.Name.ToLower()" />
@foreach (var entry in checkedTerms) {
var ti = termIndex;
<li>
@{
var disabled = IsTermDisabled(entry);
if (Model.Settings.SingleChoice) {
<input @if (disabled){ <text>disabled="disabled"</text> } type="radio" value="@entry.Id" @if (entry.Id == Model.SingleTermId){ <text>checked="checked"</text> } name="@Html.FieldNameFor(m => m.SingleTermId)" id="@Html.FieldIdFor(m => m.Terms[ti].IsChecked)" data-term="@entry.Name" data-term-identity="@entry.Name.ToLower()" />
}
else {
<input @if (disabled){ <text>disabled="disabled"</text> } type="checkbox" value="true" checked="checked" name="@Html.FieldNameFor(m => m.Terms[ti].IsChecked)" id="@Html.FieldIdFor(m => m.Terms[ti].IsChecked)" data-term="@entry.Name" data-term-identity="@entry.Name.ToLower()" />
}
}
else {
<input @if (disabled) { <text> disabled="disabled" </text> } type="checkbox" value="true" @if (entry.IsChecked) { <text> checked="checked" </text> } name="@Html.FieldNameFor(m => m.Terms[ti].IsChecked)" id="@Html.FieldIdFor(m => m.Terms[ti].IsChecked)" data-term="@entry.Name" data-term-identity="@entry.Name.ToLower()" />
}
}
<label class="forcheckbox" for="@Html.FieldIdFor(m => m.Terms[ti].IsChecked)">@entry.Name</label>
@Html.HiddenFor(m => m.Terms[ti].Id)
</li>
termIndex++;
}
<label class="forcheckbox" for="@Html.FieldIdFor(m => m.Terms[ti].IsChecked)">@entry.Name</label>
<input type="hidden" name="@Html.FieldNameFor(m => m.Terms[ti].Id)" id="@Html.FieldIdFor(m => m.Terms[ti].Id)" value="@entry.Id" />
</li>
termIndex++;
}
</ul>
</div>
@if (!Model.Terms.Any() && AuthorizedFor(Orchard.Taxonomies.Permissions.CreateTerm)) {
<div class="no-terms">
@T("There are no terms defined for {0} yet.", Model.DisplayName)
<a href="@Url.Action("Index", "TermAdmin", new { taxonomyId = Model.TaxonomyId, area = "Orchard.Taxonomies" })">@T("Create some terms")</a>
</div>
@if (!Model.HasTerms && AuthorizedFor(Orchard.Taxonomies.Permissions.CreateTerm)) {
<div class="no-terms">
@T("There are no terms defined for {0} yet.", Model.DisplayName.CamelFriendly())
<a href="@Url.Action("Index", "TermAdmin", new { taxonomyId = Model.TaxonomyId, area = "Orchard.Taxonomies" })">@T("Create some terms")</a>
</div>
}
@Html.HiddenFor(m => m.TaxonomyId)
</fieldset>