Added tag relation support to tag input

This commit is contained in:
Marcin Kurczewski 2014-10-15 21:02:25 +02:00
parent a8cb78382c
commit 15c9061562
2 changed files with 170 additions and 93 deletions

2
TODO
View file

@ -28,8 +28,6 @@ everything related to tags:
- tag editing - tag editing
- category (from config.ini) - category (from config.ini)
- description - description
- relations
- handle relations in autocomplete
refactors: refactors:
- add enum validation in IValidatables (needs refactors of enums and - add enum validation in IValidatables (needs refactors of enums and

View file

@ -6,6 +6,7 @@ App.Controls.TagInput = function($underlyingInput) {
var jQuery = App.DI.get('jQuery'); var jQuery = App.DI.get('jQuery');
var promise = App.DI.get('promise'); var promise = App.DI.get('promise');
var api = App.DI.get('api'); var api = App.DI.get('api');
var tagList = App.DI.get('tagList');
var KEY_RETURN = 13; var KEY_RETURN = 13;
var KEY_SPACE = 32; var KEY_SPACE = 32;
@ -23,7 +24,8 @@ App.Controls.TagInput = function($underlyingInput) {
var $wrapper = jQuery('<div class="tag-input">'); var $wrapper = jQuery('<div class="tag-input">');
var $tagList = jQuery('<ul class="tags">'); var $tagList = jQuery('<ul class="tags">');
var $input = jQuery('<input class="tag-real-input" type="text"/>'); var $input = jQuery('<input class="tag-real-input" type="text"/>');
var $related = jQuery('<div class="related-tags"><span>Related tags:</span><ul>'); var $siblings = jQuery('<div class="related-tags"><span>Sibling tags:</span><ul>');
var $suggestions = jQuery('<div class="related-tags"><span>Suggested tags:</span><ul>');
init(); init();
render(); render();
initAutocomplete(); initAutocomplete();
@ -54,7 +56,8 @@ App.Controls.TagInput = function($underlyingInput) {
$input.focus(); $input.focus();
}); });
$input.attr('placeholder', $underlyingInput.attr('placeholder')); $input.attr('placeholder', $underlyingInput.attr('placeholder'));
$related.insertAfter($wrapper); $suggestions.insertAfter($wrapper);
$siblings.insertAfter($wrapper);
addTagsFromText($underlyingInput.val()); addTagsFromText($underlyingInput.val());
$underlyingInput.val(''); $underlyingInput.val('');
@ -67,9 +70,8 @@ App.Controls.TagInput = function($underlyingInput) {
$input.val(''); $input.val('');
}; };
autocomplete.additionalFilter = function(results) { autocomplete.additionalFilter = function(results) {
var tags = getTags();
return _.filter(results, function(resultItem) { return _.filter(results, function(resultItem) {
return !_.contains(tags, resultItem[0]); return !_.contains(getTags(), resultItem[0]);
}); });
}; };
} }
@ -79,8 +81,8 @@ App.Controls.TagInput = function($underlyingInput) {
}); });
$input.bind('blur', function(e) { $input.bind('blur', function(e) {
$wrapper.removeClass('focused'); $wrapper.removeClass('focused');
var tag = $input.val(); var tagName = $input.val();
addTag(tag); addTag(tagName);
$input.val(''); $input.val('');
}); });
@ -98,10 +100,7 @@ App.Controls.TagInput = function($underlyingInput) {
return; return;
} }
var pastedTags = pastedText.split(/\s+/); addTagsFromTextWithoutLast(pastedText);
var lastTag = pastedTags.pop();
_.map(pastedTags, addTag);
$input.val(lastTag);
}); });
$input.bind('keydown', function(e) { $input.bind('keydown', function(e) {
@ -111,10 +110,10 @@ App.Controls.TagInput = function($underlyingInput) {
options.inputConfirmed(); options.inputConfirmed();
} }
} else if (_.contains(tagConfirmKeys, e.which)) { } else if (_.contains(tagConfirmKeys, e.which)) {
var tag = $input.val(); var tagName = $input.val();
e.preventDefault(); e.preventDefault();
$input.val(''); $input.val('');
addTag(tag); addTag(tagName);
} else if (e.which === KEY_BACKSPACE && jQuery(this).val().length === 0) { } else if (e.which === KEY_BACKSPACE && jQuery(this).val().length === 0) {
e.preventDefault(); e.preventDefault();
removeLastTag(); removeLastTag();
@ -122,17 +121,24 @@ App.Controls.TagInput = function($underlyingInput) {
}); });
function addTagsFromText(text) { function addTagsFromText(text) {
var tagsToAdd = text.split(/\s+/); var tagNamesToAdd = text.split(/\s+/);
_.map(tagsToAdd, addTag); _.map(tagNamesToAdd, addTag);
} }
function addTag(tag) { function addTagsFromTextWithoutLast(text) {
tag = tag.trim(); var tagNamesToAdd = text.split(/\s+/);
if (tag.length === 0) { var lastTagName = tagNamesToAdd.pop();
_.map(tagNamesToAdd, addTag);
$input.val(lastTagName);
}
function addTag(tagName) {
tagName = tagName.trim();
if (tagName.length === 0) {
return; return;
} }
if (tag.length > 64) { if (tagName.length > 64) {
//showing alert inside keydown event leads to mysterious behaviors //showing alert inside keydown event leads to mysterious behaviors
//in some browsers, hence the timeout //in some browsers, hence the timeout
window.setTimeout(function() { window.setTimeout(function() {
@ -141,109 +147,182 @@ App.Controls.TagInput = function($underlyingInput) {
return; return;
} }
if (isTaggedWith(tag)) { if (isTaggedWith(tagName)) {
flashTag(tag); flashTagRed(tagName);
} else { } else {
if (typeof(options.beforeTagAdded) === 'function') { beforeTagAdded(tagName);
options.beforeTagAdded(tag); tags.push(tagName);
} var $elem = createListElement(tagName);
var newTags = getTags().slice(); $tagList.append($elem);
newTags.push(tag); afterTagAdded(tagName);
setTags(newTags);
} }
} }
function removeTag(tag) { function beforeTagAdded(tagName) {
var oldTags = getTags(); if (typeof(options.beforeTagAdded) === 'function') {
var newTags = _.without(oldTags, tag); options.beforeTagAdded(tagName);
if (newTags.length !== oldTags.length) { }
}
function afterTagAdded(tagName) {
var tag = getExportedTag(tagName);
if (tag) {
_.each(tag.implications, function(impliedTagName) {
addTag(impliedTagName);
flashTagYellow(impliedTagName);
});
showOrHideSuggestions(tag.suggestions);
}
}
function getExportedTag(tagName) {
return _.first(_.filter(
tagList.getTags(),
function(t) {
return t.name.toLowerCase() === tagName.toLowerCase();
}));
}
function removeTag(tagName) {
var oldTagNames = getTags();
var newTagNames = _.without(oldTagNames, tagName);
if (newTagNames.length !== oldTagNames.length) {
if (typeof(options.beforeTagRemoved) === 'function') { if (typeof(options.beforeTagRemoved) === 'function') {
options.beforeTagRemoved(tag); options.beforeTagRemoved(tagName);
} }
setTags(newTags); setTags(newTagNames);
} }
} }
function isTaggedWith(tag) { function isTaggedWith(tagName) {
var tags = getTags(); var tagNames = _.map(getTags(), function(tagName) {
var tagNames = _.map(tags, function(tag) { return tagName.toLowerCase();
return tag.toLowerCase();
}); });
return _.contains(tagNames, tag.toLowerCase()); return _.contains(tagNames, tagName.toLowerCase());
} }
function removeLastTag() { function removeLastTag() {
removeTag(_.last(getTags())); removeTag(_.last(getTags()));
} }
function flashTag(tag) { function flashTagRed(tagName) {
var $elem = $tagList.find('li[data-tag="' + tag.toLowerCase() + '"]'); var $elem = getListElement(tagName);
$elem.css({backgroundColor: 'rgba(255, 200, 200, 1)'}); $elem.css({backgroundColor: 'rgba(255, 200, 200, 1)'});
} }
function setTags(newTags) { function flashTagYellow(tagName) {
tags = newTags.slice(); var $elem = getListElement(tagName);
$elem.css({backgroundColor: 'rgba(255, 255, 200, 1)'});
}
function getListElement(tagName) {
return $tagList.find('li[data-tag="' + tagName.toLowerCase() + '"]');
}
function setTags(newTagNames) {
tags = newTagNames.slice();
$tagList.empty(); $tagList.empty();
$underlyingInput.val(newTags.join(' ')); $underlyingInput.val(newTagNames.join(' '));
_.each(newTags, function(tag) { _.each(newTagNames, function(tagName) {
var $elem = jQuery('<li/>'); var $elem = createListElement(tagName);
$elem.attr('data-tag', tag.toLowerCase());
var $tagLink = jQuery('<a class="tag">');
$tagLink.text(tag);
$tagLink.click(function(e) {
e.preventDefault();
showRelatedTags(tag);
});
$elem.append($tagLink);
var $deleteButton = jQuery('<a class="close"><i class="fa fa-remove"></i></a>');
$deleteButton.click(function(e) {
e.preventDefault();
removeTag(tag);
$input.focus();
});
$elem.append($deleteButton);
$tagList.append($elem); $tagList.append($elem);
}); });
} }
function showRelatedTags(tag) { function createListElement(tagName) {
if ($related.data('lastTag') === tag) { var $elem = jQuery('<li/>');
$related.slideUp('fast'); $elem.attr('data-tag', tagName.toLowerCase());
$related.data('lastTag', null);
var $tagLink = jQuery('<a class="tag">');
$tagLink.text(tagName);
$tagLink.click(function(e) {
e.preventDefault();
showOrHideTagSiblings(tagName);
});
$elem.append($tagLink);
var $deleteButton = jQuery('<a class="close"><i class="fa fa-remove"></i></a>');
$deleteButton.click(function(e) {
e.preventDefault();
removeTag(tagName);
$input.focus();
});
$elem.append($deleteButton);
return $elem;
}
function showOrHideSuggestions(suggestedTagNames) {
if (_.size(suggestedTagNames) === 0) {
return; return;
} }
$related.slideUp('fast', function() { var suggestions = filterSuggestions(suggestedTagNames);
$related.data('lastTag', tag); if (suggestions.length > 0) {
var $list = $related.find('ul'); attachTagsToSuggestionList($suggestions.find('ul'), suggestions);
promise.wait(api.get('/tags/' + tag + '/siblings')) $suggestions.slideDown('fast');
.then(function(response) { }
$list.empty(); }
var relatedTags = response.json.data; function showOrHideTagSiblings(tagName) {
relatedTags = _.filter(relatedTags, function(tag) { if ($siblings.data('lastTag') === tagName) {
return !isTaggedWith(tag.name); $siblings.slideUp('fast');
}); $siblings.data('lastTag', null);
relatedTags = relatedTags.slice(0, 20); return;
_.each(relatedTags, function(tag) { }
var $li = jQuery('<li>');
var $a = jQuery('<a href="#/posts/query=' + tag.name + '">'); promise.wait(getSiblings(tagName), promise.make(function(resolve, reject) {
$a.text(tag.name); $siblings.slideUp('fast', resolve);
$a.click(function(e) { })).then(function(siblings) {
e.preventDefault(); $siblings.data('lastTag', tagName);
addTag(tag.name);
}); if (!_.size(siblings)) {
$li.append($a); return;
$list.append($li); }
});
if (_.size(relatedTags)) { var suggestions = filterSuggestions(_.pluck(siblings, 'name'));
$related.slideDown('fast'); if (suggestions.length > 0) {
attachTagsToSuggestionList($siblings.find('ul'), suggestions);
$siblings.slideDown('fast');
}
});
}
function filterSuggestions(sourceTagNames) {
var tagNames = _.filter(sourceTagNames.slice(), function(tagName) {
return !isTaggedWith(tagName);
});
tagNames = tagNames.slice(0, 20);
return tagNames;
}
function attachTagsToSuggestionList($list, tagNames) {
$list.empty();
_.each(tagNames, function(tagName) {
var $li = jQuery('<li>');
var $a = jQuery('<a href="#/posts/query=' + tagName + '">');
$a.text(tagName);
$a.click(function(e) {
e.preventDefault();
addTag(tagName);
$li.fadeOut('fast', function() {
$li.remove();
if ($list.children().length === 0) {
$list.parent('div').slideUp('fast');
} }
});
});
$li.append($a);
$list.append($li);
});
}
function getSiblings(tagName) {
return promise.make(function(resolve, reject) {
promise.wait(api.get('/tags/' + tagName + '/siblings'))
.then(function(response) {
resolve(response.json.data);
}).fail(function() { }).fail(function() {
console.log(arguments); reject();
}); });
}); });
} }