Skip to content

Commit 12fcffc

Browse files
committed
Ensure incomplete tags aren't lost on save
closes #2991, references #2172, references #2453 - moved and separated tags logic from EditorTagsView into PostTagsInputController/View - call out to PostTagsInputController when saving post to ensure an incomplete tag is completed before save - added Tab key support for suggestion selection / tag completion - don't show suggestions list when input field doesn't have focus - added code for #2172 but left commented-out as it causes side effects with completion on save - updated suggestion highlighting so it doesn't bork on html/regex chars (#2453)
1 parent 4b610f0 commit 12fcffc

File tree

6 files changed

+358
-246
lines changed

6 files changed

+358
-246
lines changed
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
var PostTagsInputController = Ember.Controller.extend({
2+
3+
tags: Ember.computed.alias('parentController.tags'),
4+
5+
suggestions: null,
6+
newTagText: null,
7+
8+
actions: {
9+
// triggered when the view is inserted so that later store.all('tag')
10+
// queries hit a full store cache and we don't see empty or out-of-date
11+
// suggestion lists
12+
loadAllTags: function () {
13+
this.store.find('tag');
14+
},
15+
16+
addNewTag: function () {
17+
var newTagText = this.get('newTagText'),
18+
searchTerm,
19+
existingTags,
20+
newTag;
21+
22+
if (Ember.isEmpty(newTagText) || this.hasTag(newTagText)) {
23+
this.send('reset');
24+
return;
25+
}
26+
27+
searchTerm = newTagText.toLowerCase();
28+
29+
// add existing tag if we have a match
30+
existingTags = this.store.all('tag').filter(function (tag) {
31+
return tag.get('name').toLowerCase() === searchTerm;
32+
});
33+
if (existingTags.get('length')) {
34+
this.send('addTag', existingTags.get('firstObject'));
35+
} else {
36+
// otherwise create a new one
37+
newTag = this.store.createRecord('tag');
38+
newTag.set('name', newTagText);
39+
this.get('tags').pushObject(newTag);
40+
}
41+
42+
this.send('reset');
43+
},
44+
45+
addTag: function (tag) {
46+
if (!Ember.isEmpty(tag) && !this.hasTag(tag.get('name'))) {
47+
this.get('tags').pushObject(tag);
48+
}
49+
this.send('reset');
50+
},
51+
52+
deleteTag: function (tag) {
53+
this.get('tags').removeObject(tag);
54+
},
55+
56+
deleteLastTag: function () {
57+
this.send('deleteTag', this.get('tags.lastObject'));
58+
},
59+
60+
selectSuggestion: function (suggestion) {
61+
if (!Ember.isEmpty(suggestion)) {
62+
this.get('suggestions').setEach('selected', false);
63+
suggestion.set('selected', true);
64+
}
65+
},
66+
67+
selectNextSuggestion: function () {
68+
var suggestions = this.get('suggestions'),
69+
selectedSuggestion = this.get('selectedSuggestion'),
70+
currentIndex,
71+
newSelection;
72+
73+
if (!Ember.isEmpty(suggestions)) {
74+
currentIndex = suggestions.indexOf(selectedSuggestion);
75+
if (currentIndex + 1 < suggestions.get('length')) {
76+
newSelection = suggestions[currentIndex + 1];
77+
this.send('selectSuggestion', newSelection);
78+
} else {
79+
suggestions.setEach('selected', false);
80+
}
81+
}
82+
},
83+
84+
selectPreviousSuggestion: function () {
85+
var suggestions = this.get('suggestions'),
86+
selectedSuggestion = this.get('selectedSuggestion'),
87+
currentIndex,
88+
lastIndex,
89+
newSelection;
90+
91+
if (!Ember.isEmpty(suggestions)) {
92+
currentIndex = suggestions.indexOf(selectedSuggestion);
93+
if (currentIndex === -1) {
94+
lastIndex = suggestions.get('length') - 1;
95+
this.send('selectSuggestion', suggestions[lastIndex]);
96+
} else if (currentIndex - 1 >= 0) {
97+
newSelection = suggestions[currentIndex - 1];
98+
this.send('selectSuggestion', newSelection);
99+
} else {
100+
suggestions.setEach('selected', false);
101+
}
102+
}
103+
},
104+
105+
addSelectedSuggestion: function () {
106+
var suggestion = this.get('selectedSuggestion');
107+
if (Ember.isEmpty(suggestion)) { return; }
108+
109+
this.send('addTag', suggestion.get('tag'));
110+
},
111+
112+
reset: function () {
113+
this.set('suggestions', null);
114+
this.set('newTagText', null);
115+
}
116+
},
117+
118+
119+
selectedSuggestion: function () {
120+
var suggestions = this.get('suggestions');
121+
if (suggestions && suggestions.get('length')) {
122+
return suggestions.filterBy('selected').get('firstObject');
123+
} else {
124+
return null;
125+
}
126+
}.property('suggestions.@each.selected'),
127+
128+
129+
updateSuggestionsList: function () {
130+
var searchTerm = this.get('newTagText'),
131+
matchingTags,
132+
// Limit the suggestions number
133+
maxSuggestions = 5,
134+
suggestions = new Ember.A();
135+
136+
if (!searchTerm || Ember.isEmpty(searchTerm.trim())) {
137+
this.set('suggestions', null);
138+
return;
139+
}
140+
141+
searchTerm = searchTerm.trim();
142+
143+
matchingTags = this.findMatchingTags(searchTerm);
144+
matchingTags = matchingTags.slice(0, maxSuggestions);
145+
matchingTags.forEach(function (matchingTag) {
146+
var suggestion = this.makeSuggestionObject(matchingTag, searchTerm);
147+
suggestions.pushObject(suggestion);
148+
}, this);
149+
150+
this.set('suggestions', suggestions);
151+
}.observes('newTagText'),
152+
153+
154+
findMatchingTags: function (searchTerm) {
155+
var matchingTags,
156+
self = this,
157+
allTags = this.store.all('tag');
158+
159+
if (allTags.get('length') === 0) {
160+
return [];
161+
}
162+
163+
searchTerm = searchTerm.toLowerCase();
164+
165+
matchingTags = allTags.filter(function (tag) {
166+
var tagNameMatches,
167+
hasAlreadyBeenAdded;
168+
169+
tagNameMatches = tag.get('name').toLowerCase().indexOf(searchTerm) !== -1;
170+
hasAlreadyBeenAdded = self.hasTag(tag.get('name'));
171+
172+
return tagNameMatches && !hasAlreadyBeenAdded;
173+
});
174+
175+
return matchingTags;
176+
},
177+
178+
hasTag: function (tagName) {
179+
return this.get('tags').mapBy('name').contains(tagName);
180+
},
181+
182+
makeSuggestionObject: function (matchingTag, _searchTerm) {
183+
var searchTerm = Ember.Handlebars.Utils.escapeExpression(_searchTerm),
184+
regexEscapedSearchTerm = searchTerm.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'),
185+
tagName = Ember.Handlebars.Utils.escapeExpression(matchingTag.get('name')),
186+
regex = new RegExp('(' + regexEscapedSearchTerm + ')', 'gi'),
187+
highlightedName,
188+
suggestion = new Ember.Object();
189+
190+
highlightedName = tagName.replace(regex, '<mark>$1</mark>');
191+
highlightedName = new Ember.Handlebars.SafeString(highlightedName);
192+
193+
suggestion.set('tag', matchingTag);
194+
suggestion.set('highlightedName', highlightedName);
195+
196+
return suggestion;
197+
},
198+
199+
});
200+
201+
export default PostTagsInputController;

core/client/mixins/editor-base-controller.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ Ember.get(PostModel, 'attributes').forEach(function (name) {
1515
watchedProps.push('tags.[]');
1616

1717
var EditorControllerMixin = Ember.Mixin.create(MarkerManager, {
18+
19+
needs: ['post-tags-input'],
20+
1821
init: function () {
1922
var self = this;
2023

@@ -120,6 +123,9 @@ var EditorControllerMixin = Ember.Mixin.create(MarkerManager, {
120123
var status = this.get('willPublish') ? 'published' : 'draft',
121124
self = this;
122125

126+
// ensure an incomplete tag is finalised before save
127+
this.get('controllers.post-tags-input').send('addNewTag');
128+
123129
// set markdown equal to what's in the editor, minus the image markers.
124130
this.set('markdown', this.getMarkdown().withoutMarkers);
125131

core/client/templates/-publish-bar.hbs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<footer id="publish-bar">
22
<nav>
3-
{{view "editor-tags" tagName="section" id="entry-tags" class="left"}}
3+
{{render 'post-tags-input'}}
44

55
<div class="right">
66

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
<label class="tag-label" for="tags" title="Tags"><span class="hidden">Tags</span></label>
22
<div class="tags">
33
{{#each tags}}
4-
<span class="tag" {{action 'tagClick' this target="view"}}>{{name}}</span>
4+
{{view view.tagView tag=this}}
55
{{/each}}
66
</div>
77
<input type="hidden" class="tags-holder" id="tags-holder">
8-
{{input type="text" id="tags" class="tag-input" value=view.input}}
9-
<ul class="suggestions overlay" {{bind-attr style=view.overlayStyle}}></ul>
8+
{{view view.tagInputView class="tag-input" id="tags" value=newTagText}}
9+
<ul class="suggestions overlay" {{bind-attr style=view.overlayStyles}}>
10+
{{#each suggestions}}
11+
{{view view.suggestionView suggestion=this}}
12+
{{/each}}
13+
</ul>

0 commit comments

Comments
 (0)