diff --git a/core/client/templates/editor-tags.hbs b/core/client/templates/post-tags-input.hbs
similarity index 56%
rename from core/client/templates/editor-tags.hbs
rename to core/client/templates/post-tags-input.hbs
index be5fd540e0..813e294b7a 100644
--- a/core/client/templates/editor-tags.hbs
+++ b/core/client/templates/post-tags-input.hbs
@@ -1,9 +1,13 @@
{{#each tags}}
- {{name}}
+ {{view view.tagView tag=this}}
{{/each}}
-{{input type="text" id="tags" class="tag-input" value=view.input}}
-
+{{view view.tagInputView class="tag-input" id="tags" value=newTagText}}
+
+ {{#each suggestions}}
+ {{view view.suggestionView suggestion=this}}
+ {{/each}}
+
diff --git a/core/client/views/editor-tags.js b/core/client/views/editor-tags.js
deleted file mode 100644
index fe06183c3b..0000000000
--- a/core/client/views/editor-tags.js
+++ /dev/null
@@ -1,242 +0,0 @@
-var EditorTags = Ember.View.extend({
- templateName: 'editor-tags',
-
- didInsertElement: function () {
- // Cache elements for later use
- this.$input = this.$('#tags');
-
- this.$suggestions = this.$('ul.suggestions');
- },
-
- willDestroyElement: function () {
- // Release ownership of the object for proper GC
- this.$input = null;
-
- this.$suggestions = null;
- },
-
- keys: {
- UP: 38,
- DOWN: 40,
- ESC: 27,
- ENTER: 13,
- BACKSPACE: 8
- },
-
- overlay: {
- visible: false,
- left: 0
- },
-
- overlayStyle: function () {
- var styles = [];
-
- styles.push(this.get('overlay.visible') ?
- 'display: block' :
- 'display: none'
- );
-
- styles.push(this.get('overlay.left') ?
- 'left: ' + this.get('overlay.left') + 'px' :
- 'left: 0'
- );
-
- return styles.join(';');
- }.property('overlay.visible'),
-
- showSuggestions: function (_searchTerm) {
- var searchTerm = _searchTerm.toLowerCase(),
- matchingTags = this.findMatchingTags(searchTerm),
- // Limit the suggestions number
- maxSuggestions = 5,
- // Escape regex special characters
- escapedTerm = searchTerm.replace(/[\-\/\\\^$*+?.()|\[\]{}]/g, '\\$&'),
- regexTerm = escapedTerm.replace(/(\s+)/g, '(<[^>]+>)*$1(<[^>]+>)*'),
- regexPattern = new RegExp('(' + regexTerm + ')', 'i'),
- highlightedNameRegex;
-
- this.set('overlay.left', this.$input.position().left);
- this.$suggestions.html('');
-
- matchingTags = matchingTags.slice(0, maxSuggestions);
- if (matchingTags.length > 0) {
- this.set('overlay.visible', true);
- }
-
- highlightedNameRegex = /(
[^<>]*)((<[^>]+>)+)([^<>]*<\/mark>)/;
-
- matchingTags.forEach(function (matchingTag) {
- var highlightedName,
- suggestionHTML;
-
- highlightedName = matchingTag.get('name').replace(regexPattern, function (match, p1) {
- return '' + encodeURIComponent(p1) + '';
- });
- /*jslint regexp: true */ // - would like to remove this
- highlightedName = highlightedName.replace(highlightedNameRegex, function (match, p1, p2, p3, p4) {
- return encodeURIComponent(p1) + '' + encodeURIComponent(p2) + '
' + encodeURIComponent(p4);
- });
-
- suggestionHTML = '' + highlightedName + '';
-
- this.$suggestions.append(suggestionHTML);
- }, this);
- },
-
- findMatchingTags: function (searchTerm) {
- var matchingTagModels,
- self = this,
- allTags = this.get('controller.store').all('tag');
-
- if (allTags.get('length') === 0) {
- return [];
- }
-
- searchTerm = searchTerm.toUpperCase();
-
- matchingTagModels = allTags.filter(function (tag) {
- var tagNameMatches,
- hasAlreadyBeenAdded;
-
- tagNameMatches = tag.get('name').toUpperCase().indexOf(searchTerm) !== -1;
-
- hasAlreadyBeenAdded = self.hasTagBeenAdded(tag.name);
-
- return tagNameMatches && !hasAlreadyBeenAdded;
- });
-
- return matchingTagModels;
- },
-
- keyDown: function (e) {
- var lastTagIndex;
-
- // Delete character tiggers on Keydown, so needed to check on that event rather than Keyup.
- if (e.keyCode === this.keys.BACKSPACE && !this.get('input')) {
- lastTagIndex = this.get('controller.model.tags').get('length') - 1;
-
- if (lastTagIndex > -1) {
- this.get('controller.model.tags').removeAt(lastTagIndex);
- }
- }
- },
-
- keyUp: function (e) {
- var searchTerm = $.trim(this.get('input'));
-
- if (e.keyCode === this.keys.UP) {
- e.preventDefault();
- if (this.get('overlay.visible')) {
- if (this.$suggestions.children('.selected').length === 0) {
- this.$suggestions.find('li:last-child').addClass('selected');
- } else {
- this.$suggestions.children('.selected').removeClass('selected').prev().addClass('selected');
- }
- }
- } else if (e.keyCode === this.keys.DOWN) {
- e.preventDefault();
- if (this.get('overlay.visible')) {
- if (this.$suggestions.children('.selected').length === 0) {
- this.$suggestions.find('li:first-child').addClass('selected');
- } else {
- this.$suggestions.children('.selected').removeClass('selected').next().addClass('selected');
- }
- }
- } else if (e.keyCode === this.keys.ESC) {
- this.set('overlay.visible', false);
- } else {
- if (searchTerm) {
- this.showSuggestions(searchTerm);
- } else {
- this.set('overlay.visible', false);
- }
- }
-
- if (e.keyCode === this.keys.UP || e.keyCode === this.keys.DOWN) {
- return false;
- }
- },
-
- keyPress: function (e) {
- var searchTerm = $.trim(this.get('input')),
- tag,
- $selectedSuggestion,
- isComma = ','.localeCompare(String.fromCharCode(e.keyCode || e.charCode)) === 0,
- hasAlreadyBeenAdded;
-
- // use localeCompare in case of international keyboard layout
- if ((e.keyCode === this.keys.ENTER || isComma) && searchTerm) {
- // Submit tag using enter or comma key
- e.preventDefault();
-
- $selectedSuggestion = this.$suggestions.children('.selected');
- if (this.get('overlay.visible') && $selectedSuggestion.length !== 0) {
- tag = {
- id: $selectedSuggestion.data('tag-id'),
- name: decodeURIComponent($selectedSuggestion.data('tag-name'))
- };
- hasAlreadyBeenAdded = this.hasTagBeenAdded(tag.name);
- if (!hasAlreadyBeenAdded) {
- this.addTag(tag);
- }
- } else {
- if (isComma) {
- // Remove comma from string if comma is used to submit.
- searchTerm = searchTerm.replace(/,/g, '');
- }
-
- hasAlreadyBeenAdded = this.hasTagBeenAdded(searchTerm);
- if (!hasAlreadyBeenAdded) {
- this.addTag({id: null, name: searchTerm});
- }
- }
- this.set('input', '');
- this.$input.focus();
- this.set('overlay.visible', false);
- }
- },
-
- addTag: function (tag) {
- var allTags = this.get('controller.store').all('tag'),
- newTag = allTags.findBy('name', tag.name);
-
- if (!newTag) {
- newTag = this.get('controller.store').createRecord('tag', tag);
- }
-
- this.get('controller.model.tags').addObject(newTag);
-
- // Wait till Ember render's the new tag to access its dom element.
- Ember.run.schedule('afterRender', this, function () {
- this.$('.tag').last()[0].scrollIntoView(true);
- window.scrollTo(0, 1);
-
- this.set('input', '');
- this.$input.focus();
-
- this.set('overlay.visible', false);
- });
- },
-
- hasTagBeenAdded: function (tagName) {
- if (!tagName) {
- return false;
- }
-
- return this.get('controller.model.tags').filter(function (usedTag) {
- return usedTag.get('name').toUpperCase() === tagName.toUpperCase();
- }).length > 0;
- },
-
- actions: {
- tagClick: function (tag) {
- this.get('controller.model.tags').removeObject(tag);
- window.scrollTo(0, 1);
- },
- }
-
-});
-
-export default EditorTags;
diff --git a/core/client/views/post-tags-input.js b/core/client/views/post-tags-input.js
new file mode 100644
index 0000000000..b17f7b285d
--- /dev/null
+++ b/core/client/views/post-tags-input.js
@@ -0,0 +1,143 @@
+var PostTagsInputView = Ember.View.extend({
+ tagName: 'section',
+ elementId: 'entry-tags',
+ classNames: 'left',
+
+ templateName: 'post-tags-input',
+
+ hasFocus: false,
+
+ keys: {
+ BACKSPACE: 8,
+ TAB: 9,
+ ENTER: 13,
+ ESCAPE: 27,
+ UP: 38,
+ DOWN: 40,
+ NUMPAD_ENTER: 108,
+ COMMA: 188
+ },
+
+ didInsertElement: function () {
+ this.get('controller').send('loadAllTags');
+ },
+
+ overlayStyles: function () {
+ var styles = [],
+ leftPos;
+
+ if (this.get('hasFocus') && this.get('controller.suggestions.length')) {
+ leftPos = this.$().find('#tags').position().left;
+ styles.push('display: block');
+ styles.push('left: ' + leftPos + 'px');
+ } else {
+ styles.push('display: none');
+ styles.push('left', 0);
+ }
+
+ return styles.join(';');
+ }.property('hasFocus', 'controller.suggestions.length'),
+
+
+ tagInputView: Ember.TextField.extend({
+ focusIn: function () {
+ this.get('parentView').set('hasFocus', true);
+ },
+
+ focusOut: function () {
+ this.get('parentView').set('hasFocus', false);
+
+ // if (!Ember.isEmpty(this.get('value'))) {
+ // this.get('parentView.controller').send('addNewTag');
+ // }
+ },
+
+ keyDown: function (event) {
+ var controller = this.get('parentView.controller'),
+ keys = this.get('parentView.keys'),
+ hasValue;
+
+ switch (event.keyCode) {
+ case keys.UP:
+ event.preventDefault();
+ controller.send('selectPreviousSuggestion');
+ break;
+
+ case keys.DOWN:
+ event.preventDefault();
+ controller.send('selectNextSuggestion');
+ break;
+
+ case keys.TAB:
+ case keys.ENTER:
+ case keys.NUMPAD_ENTER:
+ case keys.COMMA:
+ if (event.keyCode === keys.COMMA && event.shiftKey) {
+ break;
+ }
+
+ if (controller.get('selectedSuggestion')) {
+ event.preventDefault();
+ controller.send('addSelectedSuggestion');
+ } else {
+ // allow user to tab out of field if input is empty
+ hasValue = !Ember.isEmpty(this.get('value'));
+ if (hasValue || event.keyCode !== keys.TAB) {
+ event.preventDefault();
+ controller.send('addNewTag');
+ }
+ }
+ break;
+
+ case keys.BACKSPACE:
+ if (Ember.isEmpty(this.get('value'))) {
+ event.preventDefault();
+ controller.send('deleteLastTag');
+ }
+ break;
+
+ case keys.ESCAPE:
+ event.preventDefault();
+ controller.send('reset');
+ break;
+ }
+ }
+ }),
+
+
+ tagView: Ember.View.extend({
+ tagName: 'span',
+ template: Ember.Handlebars.compile('{{view.tag.name}}'),
+ classNames: 'tag',
+
+ tag: null,
+
+ click: function () {
+ this.get('parentView.controller').send('deleteTag', this.get('tag'));
+ }
+ }),
+
+
+ suggestionView: Ember.View.extend({
+ tagName: 'li',
+ template: Ember.Handlebars.compile('{{view.suggestion.highlightedName}}'),
+ classNameBindings: 'suggestion.selected',
+
+ suggestion: null,
+
+ // we can't use the 'click' event here as the focusOut event on the
+ // input will fire first
+
+ mouseDown: function (event) {
+ event.preventDefault();
+ },
+
+ mouseUp: function (event) {
+ event.preventDefault();
+ this.get('parentView.controller').send('addTag',
+ this.get('suggestion.tag'));
+ },
+ })
+});
+
+export default PostTagsInputView;