Added tag relation support to tag input
This commit is contained in:
parent
a8cb78382c
commit
15c9061562
2 changed files with 170 additions and 93 deletions
2
TODO
2
TODO
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
beforeTagAdded(tagName);
|
||||||
|
tags.push(tagName);
|
||||||
|
var $elem = createListElement(tagName);
|
||||||
|
$tagList.append($elem);
|
||||||
|
afterTagAdded(tagName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function beforeTagAdded(tagName) {
|
||||||
if (typeof(options.beforeTagAdded) === 'function') {
|
if (typeof(options.beforeTagAdded) === 'function') {
|
||||||
options.beforeTagAdded(tag);
|
options.beforeTagAdded(tagName);
|
||||||
}
|
|
||||||
var newTags = getTags().slice();
|
|
||||||
newTags.push(tag);
|
|
||||||
setTags(newTags);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeTag(tag) {
|
function afterTagAdded(tagName) {
|
||||||
var oldTags = getTags();
|
var tag = getExportedTag(tagName);
|
||||||
var newTags = _.without(oldTags, tag);
|
if (tag) {
|
||||||
if (newTags.length !== oldTags.length) {
|
_.each(tag.implications, function(impliedTagName) {
|
||||||
if (typeof(options.beforeTagRemoved) === 'function') {
|
addTag(impliedTagName);
|
||||||
options.beforeTagRemoved(tag);
|
flashTagYellow(impliedTagName);
|
||||||
}
|
|
||||||
setTags(newTags);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isTaggedWith(tag) {
|
|
||||||
var tags = getTags();
|
|
||||||
var tagNames = _.map(tags, function(tag) {
|
|
||||||
return tag.toLowerCase();
|
|
||||||
});
|
});
|
||||||
return _.contains(tagNames, tag.toLowerCase());
|
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') {
|
||||||
|
options.beforeTagRemoved(tagName);
|
||||||
|
}
|
||||||
|
setTags(newTagNames);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTaggedWith(tagName) {
|
||||||
|
var tagNames = _.map(getTags(), function(tagName) {
|
||||||
|
return tagName.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 = createListElement(tagName);
|
||||||
|
$tagList.append($elem);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createListElement(tagName) {
|
||||||
var $elem = jQuery('<li/>');
|
var $elem = jQuery('<li/>');
|
||||||
$elem.attr('data-tag', tag.toLowerCase());
|
$elem.attr('data-tag', tagName.toLowerCase());
|
||||||
|
|
||||||
var $tagLink = jQuery('<a class="tag">');
|
var $tagLink = jQuery('<a class="tag">');
|
||||||
$tagLink.text(tag);
|
$tagLink.text(tagName);
|
||||||
$tagLink.click(function(e) {
|
$tagLink.click(function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
showRelatedTags(tag);
|
showOrHideTagSiblings(tagName);
|
||||||
});
|
});
|
||||||
$elem.append($tagLink);
|
$elem.append($tagLink);
|
||||||
|
|
||||||
var $deleteButton = jQuery('<a class="close"><i class="fa fa-remove"></i></a>');
|
var $deleteButton = jQuery('<a class="close"><i class="fa fa-remove"></i></a>');
|
||||||
$deleteButton.click(function(e) {
|
$deleteButton.click(function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
removeTag(tag);
|
removeTag(tagName);
|
||||||
$input.focus();
|
$input.focus();
|
||||||
});
|
});
|
||||||
$elem.append($deleteButton);
|
$elem.append($deleteButton);
|
||||||
|
return $elem;
|
||||||
$tagList.append($elem);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showRelatedTags(tag) {
|
function showOrHideSuggestions(suggestedTagNames) {
|
||||||
if ($related.data('lastTag') === tag) {
|
if (_.size(suggestedTagNames) === 0) {
|
||||||
$related.slideUp('fast');
|
|
||||||
$related.data('lastTag', null);
|
|
||||||
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);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
promise.wait(getSiblings(tagName), promise.make(function(resolve, reject) {
|
||||||
|
$siblings.slideUp('fast', resolve);
|
||||||
|
})).then(function(siblings) {
|
||||||
|
$siblings.data('lastTag', tagName);
|
||||||
|
|
||||||
|
if (!_.size(siblings)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var suggestions = filterSuggestions(_.pluck(siblings, 'name'));
|
||||||
|
if (suggestions.length > 0) {
|
||||||
|
attachTagsToSuggestionList($siblings.find('ul'), suggestions);
|
||||||
|
$siblings.slideDown('fast');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
relatedTags = relatedTags.slice(0, 20);
|
}
|
||||||
_.each(relatedTags, function(tag) {
|
|
||||||
|
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 $li = jQuery('<li>');
|
||||||
var $a = jQuery('<a href="#/posts/query=' + tag.name + '">');
|
var $a = jQuery('<a href="#/posts/query=' + tagName + '">');
|
||||||
$a.text(tag.name);
|
$a.text(tagName);
|
||||||
$a.click(function(e) {
|
$a.click(function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
addTag(tag.name);
|
addTag(tagName);
|
||||||
|
$li.fadeOut('fast', function() {
|
||||||
|
$li.remove();
|
||||||
|
if ($list.children().length === 0) {
|
||||||
|
$list.parent('div').slideUp('fast');
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
$li.append($a);
|
$li.append($a);
|
||||||
$list.append($li);
|
$list.append($li);
|
||||||
});
|
});
|
||||||
if (_.size(relatedTags)) {
|
|
||||||
$related.slideDown('fast');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue