FEATURE: new {{mini-tag-chooser}} replaces {{tag-chooser}} in composer
This commit is contained in:
parent
0a95d2a21f
commit
6bfc25d895
|
@ -65,7 +65,7 @@
|
|||
</div>
|
||||
{{/if}}
|
||||
{{#if canEditTags}}
|
||||
{{tag-chooser tags=model.tags tabIndex="4" categoryId=model.categoryId}}
|
||||
{{mini-tag-chooser tags=model.tags tabindex="4" categoryId=model.categoryId}}
|
||||
{{/if}}
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,224 @@
|
|||
import ComboBox from "select-kit/components/combo-box";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
import { default as computed } from "ember-addons/ember-computed-decorators";
|
||||
import renderTag from "discourse/lib/render-tag";
|
||||
const { get, isEmpty, isPresent, run } = Ember;
|
||||
|
||||
export default ComboBox.extend({
|
||||
allowContentReplacement: true,
|
||||
pluginApiIdentifiers: ["mini-tag-chooser"],
|
||||
classNames: ["mini-tag-chooser"],
|
||||
classNameBindings: ["noTags"],
|
||||
verticalOffset: 3,
|
||||
filterable: true,
|
||||
noTags: Ember.computed.empty("computedTags"),
|
||||
allowAny: true,
|
||||
|
||||
init() {
|
||||
this._super();
|
||||
|
||||
this.set("termMatchesForbidden", false);
|
||||
|
||||
this.set("templateForRow", (rowComponent) => {
|
||||
const tag = rowComponent.get("computedContent");
|
||||
return renderTag(get(tag, "value"), {
|
||||
count: get(tag, "originalContent.count"),
|
||||
noHref: true
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
@computed("tags")
|
||||
computedTags(tags) {
|
||||
return Ember.makeArray(tags);
|
||||
},
|
||||
|
||||
validateCreate(term) {
|
||||
const filterRegexp = new RegExp(this.site.tags_filter_regexp, "g");
|
||||
term = term.replace(filterRegexp, "").trim().toLowerCase();
|
||||
|
||||
if (!term.length || this.get("termMatchesForbidden")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.get("siteSettings.max_tag_length") < term.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
validateSelect() {
|
||||
return this.get("computedTags").length < this.get("siteSettings.max_tags_per_topic") &&
|
||||
this.site.get("can_create_tag");
|
||||
},
|
||||
|
||||
didRender() {
|
||||
this._super();
|
||||
|
||||
this.$().on("click.mini-tag-chooser", ".selected-tag", (event) => {
|
||||
event.stopImmediatePropagation();
|
||||
this.send("removeTag", $(event.target).attr("data-value"));
|
||||
});
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super();
|
||||
|
||||
$(".select-kit-body").off("click.mini-tag-chooser");
|
||||
|
||||
const searchDebounce = this.get("searchDebounce");
|
||||
if (isPresent(searchDebounce)) { run.cancel(searchDebounce); }
|
||||
},
|
||||
|
||||
didPressEscape(event) {
|
||||
const $lastSelectedTag = $(".selected-tag.selected:last");
|
||||
|
||||
if ($lastSelectedTag && this.get("isExpanded")) {
|
||||
$lastSelectedTag.removeClass("selected");
|
||||
this._destroyEvent(event);
|
||||
} else {
|
||||
this._super(event);
|
||||
}
|
||||
},
|
||||
|
||||
didPressBackspace() {
|
||||
if (!this.get("isExpanded")) {
|
||||
this.expand();
|
||||
return;
|
||||
}
|
||||
|
||||
const $lastSelectedTag = $(".selected-tag:last");
|
||||
|
||||
if (!isEmpty(this.get("filter"))) {
|
||||
$lastSelectedTag.removeClass("is-highlighted");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$lastSelectedTag.length) return;
|
||||
|
||||
if (!$lastSelectedTag.hasClass("is-highlighted")) {
|
||||
$lastSelectedTag.addClass("is-highlighted");
|
||||
} else {
|
||||
this.send("removeTag", $lastSelectedTag.attr("data-value"));
|
||||
}
|
||||
},
|
||||
|
||||
@computed("tags.[]", "filter")
|
||||
collectionHeader(tags, filter) {
|
||||
if (!Ember.isEmpty(tags)) {
|
||||
let output = "";
|
||||
|
||||
if (tags.length >= 20) {
|
||||
tags = tags.filter(t => t.indexOf(filter) >= 0);
|
||||
}
|
||||
|
||||
tags.map((tag) => {
|
||||
output += `
|
||||
<button class="selected-tag" data-value="${tag}">
|
||||
${tag}
|
||||
</button>
|
||||
`;
|
||||
});
|
||||
|
||||
return `<div class="selected-tags">${output}</div>`;
|
||||
}
|
||||
},
|
||||
|
||||
computeHeaderContent() {
|
||||
let content = this.baseHeaderComputedContent();
|
||||
|
||||
if (isEmpty(this.get("computedTags"))) {
|
||||
content.label = I18n.t("tagging.choose_for_topic");
|
||||
} else {
|
||||
content.label = this.get("computedTags").join(",");
|
||||
}
|
||||
|
||||
return content;
|
||||
},
|
||||
|
||||
actions: {
|
||||
removeTag(tag) {
|
||||
let tags = this.get("computedTags");
|
||||
delete tags[tags.indexOf(tag)];
|
||||
this.set("tags", tags.filter(t => t));
|
||||
this.set("content", []);
|
||||
this.set("searchDebounce", run.debounce(this, this._searchTags, 200));
|
||||
},
|
||||
|
||||
onExpand() {
|
||||
this.set("searchDebounce", run.debounce(this, this._searchTags, 200));
|
||||
},
|
||||
|
||||
onFilter(filter) {
|
||||
filter = isEmpty(filter) ? null : filter;
|
||||
this.set("searchDebounce", run.debounce(this, this._searchTags, filter, 200));
|
||||
},
|
||||
|
||||
onSelect(tag) {
|
||||
if (isEmpty(this.get("computedTags"))) {
|
||||
this.set("tags", Ember.makeArray(tag));
|
||||
} else {
|
||||
this.set("tags", this.get("computedTags").concat(tag));
|
||||
}
|
||||
|
||||
this.set("content", []);
|
||||
this.set("searchDebounce", run.debounce(this, this._searchTags, 200));
|
||||
}
|
||||
},
|
||||
|
||||
muateAttributes() {
|
||||
this.set("value", null);
|
||||
},
|
||||
|
||||
_searchTags(query) {
|
||||
this.startLoading();
|
||||
|
||||
const selectedTags = Ember.makeArray(this.get("computedTags")).filter(t => t);
|
||||
|
||||
const self = this;
|
||||
|
||||
const sortTags = this.siteSettings.tags_sort_alphabetically;
|
||||
|
||||
const data = {
|
||||
q: query,
|
||||
limit: this.siteSettings.max_tag_search_results,
|
||||
categoryId: this.get("categoryId")
|
||||
};
|
||||
|
||||
if (selectedTags) {
|
||||
data.selected_tags = selectedTags.slice(0, 100);
|
||||
}
|
||||
|
||||
ajax(Discourse.getURL("/tags/filter/search"), {
|
||||
quietMillis: 200,
|
||||
cache: true,
|
||||
dataType: "json",
|
||||
data,
|
||||
}).then(json => {
|
||||
let results = json.results;
|
||||
|
||||
self.set("termMatchesForbidden", json.forbidden ? true : false);
|
||||
|
||||
if (sortTags) {
|
||||
results = results.sort((a, b) => a.id > b.id);
|
||||
}
|
||||
|
||||
const content = results.map((result) => {
|
||||
return {
|
||||
id: result.text,
|
||||
name: result.text,
|
||||
count: result.count
|
||||
};
|
||||
}).filter(c => !selectedTags.includes(c.id));
|
||||
|
||||
self.set("content", content);
|
||||
self.stopLoading();
|
||||
this.autoHighlight();
|
||||
}).catch(error => {
|
||||
self.stopLoading();
|
||||
popupAjaxError(error);
|
||||
});
|
||||
}
|
||||
});
|
|
@ -252,10 +252,6 @@ export default SelectKitComponent.extend({
|
|||
this.autoHighlight();
|
||||
},
|
||||
|
||||
validateComputedContentItem(computedContentItem) {
|
||||
return !this.get("computedValues").includes(computedContentItem.value);
|
||||
},
|
||||
|
||||
actions: {
|
||||
clearSelection() {
|
||||
this.send("deselect", this.get("selectedComputedContents"));
|
||||
|
@ -263,7 +259,8 @@ export default SelectKitComponent.extend({
|
|||
},
|
||||
|
||||
create(computedContentItem) {
|
||||
if (this.validateComputedContentItem(computedContentItem)) {
|
||||
if (!this.get("computedValues").includes(computedContentItem.value) &&
|
||||
this.validateCreate(computedContentItem.value)) {
|
||||
this.get("computedContent").pushObject(computedContentItem);
|
||||
this._boundaryActionHandler("onCreate");
|
||||
this.send("select", computedContentItem);
|
||||
|
@ -274,9 +271,14 @@ export default SelectKitComponent.extend({
|
|||
|
||||
select(computedContentItem) {
|
||||
this.willSelect(computedContentItem);
|
||||
|
||||
if (this.validateSelect(computedContentItem)) {
|
||||
this.get("computedValues").pushObject(computedContentItem.value);
|
||||
Ember.run.next(() => this.mutateAttributes());
|
||||
Ember.run.schedule("afterRender", () => this.didSelect(computedContentItem));
|
||||
} else {
|
||||
this._boundaryActionHandler("onSelectFailure");
|
||||
}
|
||||
},
|
||||
|
||||
deselect(rowComputedContentItems) {
|
||||
|
|
|
@ -16,6 +16,7 @@ export default Ember.Component.extend(UtilsMixin, PluginApiMixin, DomHelpersMixi
|
|||
layoutName: "select-kit/templates/components/select-kit",
|
||||
classNames: ["select-kit"],
|
||||
classNameBindings: [
|
||||
"isLoading",
|
||||
"isFocused",
|
||||
"isExpanded",
|
||||
"isDisabled",
|
||||
|
@ -30,6 +31,7 @@ export default Ember.Component.extend(UtilsMixin, PluginApiMixin, DomHelpersMixi
|
|||
isExpanded: false,
|
||||
isFocused: false,
|
||||
isHidden: false,
|
||||
isLoading: false,
|
||||
renderedBodyOnce: false,
|
||||
renderedFilterOnce: false,
|
||||
tabindex: 0,
|
||||
|
@ -41,6 +43,7 @@ export default Ember.Component.extend(UtilsMixin, PluginApiMixin, DomHelpersMixi
|
|||
autoFilterable: false,
|
||||
filterable: false,
|
||||
filter: "",
|
||||
previousFilter: null,
|
||||
filterPlaceholder: "select_kit.filter_placeholder",
|
||||
filterIcon: "search",
|
||||
headerIcon: null,
|
||||
|
@ -118,6 +121,10 @@ export default Ember.Component.extend(UtilsMixin, PluginApiMixin, DomHelpersMixi
|
|||
return this.baseComputedContentItem(contentItem, options);
|
||||
},
|
||||
|
||||
validateCreate() { return true; },
|
||||
|
||||
validateSelect() { return true; },
|
||||
|
||||
baseComputedContentItem(contentItem, options) {
|
||||
let originalContent;
|
||||
options = options || {};
|
||||
|
@ -163,7 +170,7 @@ export default Ember.Component.extend(UtilsMixin, PluginApiMixin, DomHelpersMixi
|
|||
@computed("filter", "computedContent")
|
||||
shouldDisplayCreateRow(filter, computedContent) {
|
||||
if (computedContent.map(c => c.value).includes(filter)) return false;
|
||||
if (this.get("allowAny") && filter.length > 0) return true;
|
||||
if (this.get("allowAny") && filter.length > 0 && this.validateCreate(filter)) return true;
|
||||
return false;
|
||||
},
|
||||
|
||||
|
@ -184,7 +191,7 @@ export default Ember.Component.extend(UtilsMixin, PluginApiMixin, DomHelpersMixi
|
|||
@computed("filter")
|
||||
templateForCreateRow() {
|
||||
return (rowComponent) => {
|
||||
return I18n.t("select_box.create", {
|
||||
return I18n.t("select_kit.create", {
|
||||
content: rowComponent.get("computedContent.name")
|
||||
});
|
||||
};
|
||||
|
@ -238,6 +245,16 @@ export default Ember.Component.extend(UtilsMixin, PluginApiMixin, DomHelpersMixi
|
|||
this.setProperties({ filter: "" });
|
||||
},
|
||||
|
||||
startLoading() {
|
||||
this.set("isLoading", true);
|
||||
this._boundaryActionHandler("onStartLoading");
|
||||
},
|
||||
|
||||
stopLoading() {
|
||||
this.set("isLoading", false);
|
||||
this._boundaryActionHandler("onStopLoading");
|
||||
},
|
||||
|
||||
_setCollectionHeaderComputedContent() {
|
||||
const collectionHeaderComputedContent = applyCollectionHeaderCallbacks(
|
||||
this.get("pluginApiIdentifiers"),
|
||||
|
@ -283,9 +300,12 @@ export default Ember.Component.extend(UtilsMixin, PluginApiMixin, DomHelpersMixi
|
|||
},
|
||||
|
||||
filterComputedContent(filter) {
|
||||
if (filter === this.get("previousFilter")) return;
|
||||
|
||||
this.setProperties({
|
||||
highlightedValue: null,
|
||||
renderedFilterOnce: true,
|
||||
previousFilter: filter,
|
||||
filter
|
||||
});
|
||||
this.autoHighlight();
|
||||
|
|
|
@ -145,10 +145,6 @@ export default SelectKitComponent.extend({
|
|||
});
|
||||
},
|
||||
|
||||
validateComputedContentItem(computedContentItem) {
|
||||
return this.get("computedValue") !== computedContentItem.value;
|
||||
},
|
||||
|
||||
actions: {
|
||||
clearSelection() {
|
||||
this.send("deselect", this.get("selectedComputedContent"));
|
||||
|
@ -156,7 +152,8 @@ export default SelectKitComponent.extend({
|
|||
},
|
||||
|
||||
create(computedContentItem) {
|
||||
if (this.validateComputedContentItem(computedContentItem)) {
|
||||
if (this.get("computedValue") !== computedContentItem.value &&
|
||||
this.validateCreate(computedContentItem.value)) {
|
||||
this.get("computedContent").pushObject(computedContentItem);
|
||||
this._boundaryActionHandler("onCreate");
|
||||
this.send("select", computedContentItem);
|
||||
|
@ -166,10 +163,14 @@ export default SelectKitComponent.extend({
|
|||
},
|
||||
|
||||
select(rowComputedContentItem) {
|
||||
if (this.validateSelect(rowComputedContentItem)) {
|
||||
this.willSelect(rowComputedContentItem);
|
||||
this.set("computedValue", rowComputedContentItem.value);
|
||||
this.mutateAttributes();
|
||||
run.schedule("afterRender", () => this.didSelect(rowComputedContentItem));
|
||||
} else {
|
||||
this._boundaryActionHandler("onSelectFailure");
|
||||
}
|
||||
},
|
||||
|
||||
deselect(rowComputedContentItem) {
|
||||
|
|
|
@ -108,6 +108,7 @@ export default Ember.Mixin.create({
|
|||
.on("keydown.select-kit", (event) => {
|
||||
const keyCode = event.keyCode || event.which;
|
||||
|
||||
if (keyCode === this.keys.BACKSPACE) this.backspaceFromFilter(event);
|
||||
if (keyCode === this.keys.TAB) this.tabFromFilter(event);
|
||||
if (keyCode === this.keys.ESC) this.escapeFromFilter(event);
|
||||
if (keyCode === this.keys.ENTER) this.enterFromFilter(event);
|
||||
|
@ -207,6 +208,7 @@ export default Ember.Mixin.create({
|
|||
upAndDownFromFilter(event) { this.didPressUpAndDownArrows(event); },
|
||||
|
||||
backspaceFromHeader(event) { this.didPressBackspace(event); },
|
||||
backspaceFromFilter(event) { this.didPressBackspace(event); },
|
||||
|
||||
enterFromHeader(event) { this.didPressEnter(event); },
|
||||
enterFromFilter(event) { this.didPressEnter(event); },
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
computedContent=headerComputedContent
|
||||
deselect=(action "deselect")
|
||||
toggle=(action "toggle")
|
||||
isLoading=isLoading
|
||||
filterComputedContent=(action "filterComputedContent")
|
||||
clearSelection=(action "clearSelection")
|
||||
options=headerComponentOptions
|
||||
|
@ -14,6 +15,7 @@
|
|||
<div class="select-kit-body">
|
||||
{{component filterComponent
|
||||
filter=filter
|
||||
isLoading=isLoading
|
||||
icon=filterIcon
|
||||
shouldDisplayFilter=shouldDisplayFilter
|
||||
placeholder=(i18n filterPlaceholder)
|
||||
|
|
|
@ -10,6 +10,10 @@
|
|||
value=filter
|
||||
}}
|
||||
|
||||
{{#if isLoading}}
|
||||
{{loading-spinner size="small"}}
|
||||
{{else}}
|
||||
{{#if icon}}
|
||||
{{d-icon icon class="filter-icon"}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
|
|
@ -221,9 +221,9 @@
|
|||
flex-basis: 50%;
|
||||
}
|
||||
|
||||
.tag-chooser {
|
||||
.mini-tag-chooser {
|
||||
flex: 1 1 25%;
|
||||
margin: 0 0 5px 10px;
|
||||
margin: 0 0 5px 5px;
|
||||
background: $secondary;
|
||||
@media all and (max-width: 900px) {
|
||||
margin: 0;
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
.select-kit {
|
||||
&.combo-box {
|
||||
&.mini-tag-chooser {
|
||||
margin-bottom: 5px;
|
||||
margin-left: 5px;
|
||||
|
||||
&.is-expanded {
|
||||
.select-kit-header {
|
||||
border: 1px solid $tertiary;
|
||||
-webkit-box-shadow: $tertiary 0 0 6px 0px;
|
||||
box-shadow: $tertiary 0 0 6px 0px;
|
||||
}
|
||||
}
|
||||
|
||||
&.no-tags {
|
||||
.select-kit-header .selected-name {
|
||||
color: $primary-medium;
|
||||
}
|
||||
}
|
||||
|
||||
.select-kit-body {
|
||||
max-width: 500px;
|
||||
width: 500px;
|
||||
border: 1px solid $primary-low;
|
||||
}
|
||||
|
||||
.select-kit-filter {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.select-kit-wrapper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.select-kit-row {
|
||||
&.is-selected {
|
||||
background: none;
|
||||
}
|
||||
|
||||
&.is-highlighted.is-selected {
|
||||
background: $tertiary-low;
|
||||
}
|
||||
|
||||
.discourse-tag-count {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.select-kit-collection {
|
||||
.collection-header {
|
||||
max-height: 125px;
|
||||
overflow-y: auto;
|
||||
|
||||
.selected-tags {
|
||||
display: flex;
|
||||
padding: 3px;
|
||||
flex-wrap: wrap;
|
||||
border-bottom: 1px solid $primary-low;
|
||||
}
|
||||
|
||||
.selected-tag {
|
||||
background: $primary-low;
|
||||
border-radius: 2px;
|
||||
padding: 2px 4px;
|
||||
margin: 2px;
|
||||
border: 0;
|
||||
|
||||
&.is-highlighted {
|
||||
box-shadow: 0 0 2px $danger, 0 1px 0 rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: '\f00d';
|
||||
font-family: 'FontAwesome';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue