diff --git a/core/client/controllers/post-tags-input.js b/core/client/controllers/post-tags-input.js new file mode 100644 index 0000000000..e60fb5a65b --- /dev/null +++ b/core/client/controllers/post-tags-input.js @@ -0,0 +1,201 @@ +var PostTagsInputController = Ember.Controller.extend({ + + tags: Ember.computed.alias('parentController.tags'), + + suggestions: null, + newTagText: null, + + actions: { + // triggered when the view is inserted so that later store.all('tag') + // queries hit a full store cache and we don't see empty or out-of-date + // suggestion lists + loadAllTags: function () { + this.store.find('tag'); + }, + + addNewTag: function () { + var newTagText = this.get('newTagText'), + searchTerm, + existingTags, + newTag; + + if (Ember.isEmpty(newTagText) || this.hasTag(newTagText)) { + this.send('reset'); + return; + } + + searchTerm = newTagText.toLowerCase(); + + // add existing tag if we have a match + existingTags = this.store.all('tag').filter(function (tag) { + return tag.get('name').toLowerCase() === searchTerm; + }); + if (existingTags.get('length')) { + this.send('addTag', existingTags.get('firstObject')); + } else { + // otherwise create a new one + newTag = this.store.createRecord('tag'); + newTag.set('name', newTagText); + this.get('tags').pushObject(newTag); + } + + this.send('reset'); + }, + + addTag: function (tag) { + if (!Ember.isEmpty(tag) && !this.hasTag(tag.get('name'))) { + this.get('tags').pushObject(tag); + } + this.send('reset'); + }, + + deleteTag: function (tag) { + this.get('tags').removeObject(tag); + }, + + deleteLastTag: function () { + this.send('deleteTag', this.get('tags.lastObject')); + }, + + selectSuggestion: function (suggestion) { + if (!Ember.isEmpty(suggestion)) { + this.get('suggestions').setEach('selected', false); + suggestion.set('selected', true); + } + }, + + selectNextSuggestion: function () { + var suggestions = this.get('suggestions'), + selectedSuggestion = this.get('selectedSuggestion'), + currentIndex, + newSelection; + + if (!Ember.isEmpty(suggestions)) { + currentIndex = suggestions.indexOf(selectedSuggestion); + if (currentIndex + 1 < suggestions.get('length')) { + newSelection = suggestions[currentIndex + 1]; + this.send('selectSuggestion', newSelection); + } else { + suggestions.setEach('selected', false); + } + } + }, + + selectPreviousSuggestion: function () { + var suggestions = this.get('suggestions'), + selectedSuggestion = this.get('selectedSuggestion'), + currentIndex, + lastIndex, + newSelection; + + if (!Ember.isEmpty(suggestions)) { + currentIndex = suggestions.indexOf(selectedSuggestion); + if (currentIndex === -1) { + lastIndex = suggestions.get('length') - 1; + this.send('selectSuggestion', suggestions[lastIndex]); + } else if (currentIndex - 1 >= 0) { + newSelection = suggestions[currentIndex - 1]; + this.send('selectSuggestion', newSelection); + } else { + suggestions.setEach('selected', false); + } + } + }, + + addSelectedSuggestion: function () { + var suggestion = this.get('selectedSuggestion'); + if (Ember.isEmpty(suggestion)) { return; } + + this.send('addTag', suggestion.get('tag')); + }, + + reset: function () { + this.set('suggestions', null); + this.set('newTagText', null); + } + }, + + + selectedSuggestion: function () { + var suggestions = this.get('suggestions'); + if (suggestions && suggestions.get('length')) { + return suggestions.filterBy('selected').get('firstObject'); + } else { + return null; + } + }.property('suggestions.@each.selected'), + + + updateSuggestionsList: function () { + var searchTerm = this.get('newTagText'), + matchingTags, + // Limit the suggestions number + maxSuggestions = 5, + suggestions = new Ember.A(); + + if (!searchTerm || Ember.isEmpty(searchTerm.trim())) { + this.set('suggestions', null); + return; + } + + searchTerm = searchTerm.trim(); + + matchingTags = this.findMatchingTags(searchTerm); + matchingTags = matchingTags.slice(0, maxSuggestions); + matchingTags.forEach(function (matchingTag) { + var suggestion = this.makeSuggestionObject(matchingTag, searchTerm); + suggestions.pushObject(suggestion); + }, this); + + this.set('suggestions', suggestions); + }.observes('newTagText'), + + + findMatchingTags: function (searchTerm) { + var matchingTags, + self = this, + allTags = this.store.all('tag'); + + if (allTags.get('length') === 0) { + return []; + } + + searchTerm = searchTerm.toLowerCase(); + + matchingTags = allTags.filter(function (tag) { + var tagNameMatches, + hasAlreadyBeenAdded; + + tagNameMatches = tag.get('name').toLowerCase().indexOf(searchTerm) !== -1; + hasAlreadyBeenAdded = self.hasTag(tag.get('name')); + + return tagNameMatches && !hasAlreadyBeenAdded; + }); + + return matchingTags; + }, + + hasTag: function (tagName) { + return this.get('tags').mapBy('name').contains(tagName); + }, + + makeSuggestionObject: function (matchingTag, _searchTerm) { + var searchTerm = Ember.Handlebars.Utils.escapeExpression(_searchTerm), + regexEscapedSearchTerm = searchTerm.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'), + tagName = Ember.Handlebars.Utils.escapeExpression(matchingTag.get('name')), + regex = new RegExp('(' + regexEscapedSearchTerm + ')', 'gi'), + highlightedName, + suggestion = new Ember.Object(); + + highlightedName = tagName.replace(regex, '$1'); + highlightedName = new Ember.Handlebars.SafeString(highlightedName); + + suggestion.set('tag', matchingTag); + suggestion.set('highlightedName', highlightedName); + + return suggestion; + }, + +}); + +export default PostTagsInputController; diff --git a/core/client/mixins/editor-base-controller.js b/core/client/mixins/editor-base-controller.js index e4ab90ec96..54428e45dd 100644 --- a/core/client/mixins/editor-base-controller.js +++ b/core/client/mixins/editor-base-controller.js @@ -15,6 +15,9 @@ Ember.get(PostModel, 'attributes').forEach(function (name) { watchedProps.push('tags.[]'); var EditorControllerMixin = Ember.Mixin.create(MarkerManager, { + + needs: ['post-tags-input'], + init: function () { var self = this; @@ -120,6 +123,9 @@ var EditorControllerMixin = Ember.Mixin.create(MarkerManager, { var status = this.get('willPublish') ? 'published' : 'draft', self = this; + // ensure an incomplete tag is finalised before save + this.get('controllers.post-tags-input').send('addNewTag'); + // set markdown equal to what's in the editor, minus the image markers. this.set('markdown', this.getMarkdown().withoutMarkers); diff --git a/core/client/templates/-publish-bar.hbs b/core/client/templates/-publish-bar.hbs index fb00c63886..925cb31936 100644 --- a/core/client/templates/-publish-bar.hbs +++ b/core/client/templates/-publish-bar.hbs @@ -1,6 +1,6 @@