Added autocompletion
This commit is contained in:
parent
b57fee0ad8
commit
adfc120642
11 changed files with 320 additions and 31 deletions
3
TODO
3
TODO
|
@ -20,8 +20,9 @@ everything related to posts:
|
|||
(move post snapshot factory methods to PostService)
|
||||
|
||||
everything related to tags:
|
||||
- automatic tag removal
|
||||
- tag refresh when editing post
|
||||
- tag listing
|
||||
- tag autocompletion (difficult)
|
||||
- time of last tag usage
|
||||
- time of tag addition
|
||||
- mass tag
|
||||
|
|
23
public_html/css/tags.css
Normal file
23
public_html/css/tags.css
Normal file
|
@ -0,0 +1,23 @@
|
|||
.autocomplete {
|
||||
position: absolute;
|
||||
display: none;
|
||||
z-index: 10;
|
||||
}
|
||||
.autocomplete ul {
|
||||
list-style-type: none;
|
||||
padding: 0 0 !important;
|
||||
margin: 0 !important;
|
||||
border: 2px solid #5da;
|
||||
background: white;
|
||||
display: block !important;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.autocomplete li {
|
||||
margin: 0;
|
||||
padding: 0.1em 0.5em !important;
|
||||
}
|
||||
|
||||
.autocomplete li.active {
|
||||
background: #5da;
|
||||
}
|
|
@ -37,6 +37,7 @@
|
|||
<link rel="stylesheet" type="text/css" href="/css/home.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="/css/history.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="/css/comments.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="/css/tags.css"/>
|
||||
<!-- /build -->
|
||||
|
||||
<!-- build:remove -->
|
||||
|
@ -86,9 +87,11 @@
|
|||
<script type="text/javascript" src="/js/Pager.js"></script>
|
||||
<script type="text/javascript" src="/js/BrowsingSettings.js"></script>
|
||||
<script type="text/javascript" src="/js/Controls/FileDropper.js"></script>
|
||||
<script type="text/javascript" src="/js/Controls/AutoCompleteInput.js"></script>
|
||||
<script type="text/javascript" src="/js/Controls/TagInput.js"></script>
|
||||
<script type="text/javascript" src="/js/PresenterManager.js"></script>
|
||||
|
||||
<script type="text/javascript" src="/js/Services/TagList.js"></script>
|
||||
<script type="text/javascript" src="/js/Services/PostsAroundCalculator.js"></script>
|
||||
|
||||
<script type="text/javascript" src="/js/Presenters/TopNavigationPresenter.js"></script>
|
||||
|
|
223
public_html/js/Controls/AutoCompleteInput.js
Normal file
223
public_html/js/Controls/AutoCompleteInput.js
Normal file
|
@ -0,0 +1,223 @@
|
|||
var App = App || {};
|
||||
App.Controls = App.Controls || {};
|
||||
|
||||
App.Controls.AutoCompleteInput = function($input) {
|
||||
var _ = App.DI.get('_');
|
||||
var jQuery = App.DI.get('jQuery');
|
||||
var tagList = App.DI.get('tagList');
|
||||
|
||||
var KEY_RETURN = 13;
|
||||
var KEY_ESCAPE = 27;
|
||||
var KEY_UP = 38;
|
||||
var KEY_DOWN = 40;
|
||||
|
||||
var options = {
|
||||
caseSensitive: false,
|
||||
source: null,
|
||||
maxResults: 15,
|
||||
minLengthToArbitrarySearch: 3,
|
||||
onApply: null,
|
||||
additionalFilter: null,
|
||||
};
|
||||
var showTimeout = null;
|
||||
var cachedSource = null;
|
||||
var results = [];
|
||||
var activeResult = -1;
|
||||
|
||||
if ($input.length === 0) {
|
||||
throw new Error('Input element was not found');
|
||||
}
|
||||
if ($input.length > 1) {
|
||||
throw new Error('Cannot add autocompletion to more than one element at once');
|
||||
}
|
||||
if ($input.attr('data-autocomplete')) {
|
||||
throw new Error('Autocompletion was already added for this element');
|
||||
}
|
||||
$input.attr('data-autocomplete', true);
|
||||
$input.attr('autocomplete', 'off');
|
||||
|
||||
var $div = jQuery('<div>');
|
||||
var $list = jQuery('<ul>');
|
||||
$div.addClass('autocomplete');
|
||||
$div.append($list);
|
||||
jQuery(document.body).append($div);
|
||||
|
||||
function getSource() {
|
||||
if (cachedSource) {
|
||||
return cachedSource;
|
||||
} else {
|
||||
var source = source || tagList.getTags();
|
||||
source = _.pairs(source);
|
||||
source = _.sortBy(source, function(a) { return -a[1]; });
|
||||
source = _.filter(source, function(a) { return a[1] > 0; });
|
||||
source = _.map(source, function(a) { return [a[0], a[0] + ' (' + a[1] + ')']; });
|
||||
cachedSource = source;
|
||||
return source;
|
||||
}
|
||||
}
|
||||
|
||||
$input.bind('keydown', function(e) {
|
||||
if (isShown() && e.which === KEY_ESCAPE) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
hide();
|
||||
} else if (isShown() && e.which === KEY_DOWN) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
selectNext();
|
||||
} else if (isShown() && e.which === KEY_UP) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
selectPrevious();
|
||||
} else if (isShown() && e.which === KEY_RETURN && activeResult >= 0) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
applyAutocomplete();
|
||||
hide();
|
||||
} else {
|
||||
window.clearTimeout(showTimeout);
|
||||
showTimeout = window.setTimeout(showOrHide, 250);
|
||||
}
|
||||
});
|
||||
|
||||
$input.blur(function(e) {
|
||||
window.clearTimeout(showTimeout);
|
||||
window.setTimeout(function() { hide(); }, 50);
|
||||
});
|
||||
|
||||
function getSelectionStart(){
|
||||
var input = $input.get(0);
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
if ('selectionStart' in input) {
|
||||
return input.selectionStart;
|
||||
} else if (document.selection) {
|
||||
input.focus();
|
||||
var sel = document.selection.createRange();
|
||||
var selLen = document.selection.createRange().text.length;
|
||||
sel.moveStart('character', -input.value.length);
|
||||
return sel.text.length - selLen;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function getTextToFind() {
|
||||
var val = $input.val();
|
||||
var start = getSelectionStart();
|
||||
return val.substring(0, start).replace(/.*\s/, '');
|
||||
}
|
||||
|
||||
function showOrHide() {
|
||||
var textToFind = getTextToFind();
|
||||
if (textToFind.length === 0) {
|
||||
hide();
|
||||
} else {
|
||||
updateResults(textToFind);
|
||||
refreshList();
|
||||
}
|
||||
}
|
||||
|
||||
function isShown() {
|
||||
return $div.is(':visible');
|
||||
}
|
||||
|
||||
function hide() {
|
||||
$div.hide();
|
||||
}
|
||||
|
||||
function selectPrevious() {
|
||||
select(activeResult === -1 ? results.length - 1 : activeResult - 1);
|
||||
}
|
||||
|
||||
function selectNext() {
|
||||
select(activeResult === -1 ? 0 : activeResult + 1);
|
||||
}
|
||||
|
||||
function select(newActiveResult) {
|
||||
if (newActiveResult >= 0 && newActiveResult < results.length) {
|
||||
activeResult = newActiveResult;
|
||||
refreshActiveResult();
|
||||
} else {
|
||||
activeResult = - 1;
|
||||
refreshActiveResult();
|
||||
}
|
||||
}
|
||||
|
||||
function getResultsFilter(textToFind) {
|
||||
if (textToFind.length < options.minLengthToArbitrarySearch) {
|
||||
return options.caseSensitive ?
|
||||
function(resultItem) { return resultItem[0].indexOf(textToFind) === 0; } :
|
||||
function(resultItem) { return resultItem[0].toLowerCase().indexOf(textToFind.toLowerCase()) === 0; };
|
||||
} else {
|
||||
return options.caseSensitive ?
|
||||
function(resultItem) { return resultItem[0].indexOf(textToFind) >= 0; } :
|
||||
function(resultItem) { return resultItem[0].toLowerCase().indexOf(textToFind.toLowerCase()) >= 0; };
|
||||
}
|
||||
}
|
||||
|
||||
function updateResults(textToFind) {
|
||||
var source = getSource();
|
||||
var filter = getResultsFilter(textToFind);
|
||||
results = _.filter(source, filter);
|
||||
if (options.additionalFilter) {
|
||||
results = options.additionalFilter(results);
|
||||
}
|
||||
results = results.slice(0, options.maxResults);
|
||||
activeResult = -1;
|
||||
}
|
||||
|
||||
function applyAutocomplete() {
|
||||
if (options.onApply) {
|
||||
options.onApply(results[activeResult][0]);
|
||||
} else {
|
||||
var val = $input.val();
|
||||
var start = getSelectionStart();
|
||||
var prefix = '';
|
||||
var suffix = val.substring(start);
|
||||
var middle = val.substring(0, start);
|
||||
var index = middle.lastIndexOf(' ');
|
||||
if (index !== -1) {
|
||||
prefix = val.substring(0, index + 1);
|
||||
middle = val.substring(index + 1);
|
||||
}
|
||||
$input.val(prefix + results[activeResult][0] + ' ' + suffix.trimLeft());
|
||||
$input.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function refreshList() {
|
||||
if (results.length === 0) {
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
|
||||
$list.empty();
|
||||
_.each(results, function(resultItem) {
|
||||
var $listItem = jQuery('<li/>');
|
||||
$listItem.text(resultItem[1]);
|
||||
$listItem.attr('data-key', resultItem[0]);
|
||||
$list.append($listItem);
|
||||
});
|
||||
refreshActiveResult();
|
||||
$div.css({
|
||||
left: ($input.offset().left) + 'px',
|
||||
top: ($input.offset().top + $input.outerHeight() - 2) + 'px',
|
||||
});
|
||||
$div.show();
|
||||
}
|
||||
|
||||
function refreshActiveResult() {
|
||||
$list.find('li.active').removeClass('active');
|
||||
if (activeResult >= 0) {
|
||||
$list.find('li').eq(activeResult).addClass('active');
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
};
|
|
@ -1,10 +1,9 @@
|
|||
var App = App || {};
|
||||
App.Controls = App.Controls || {};
|
||||
|
||||
App.Controls.FileDropper = function(
|
||||
$fileInput,
|
||||
_,
|
||||
jQuery) {
|
||||
App.Controls.FileDropper = function($fileInput) {
|
||||
var _ = App.DI.get('_');
|
||||
var jQuery = App.DI.get('jQuery');
|
||||
|
||||
var options = {
|
||||
onChange: null,
|
||||
|
@ -63,7 +62,4 @@ App.Controls.FileDropper = function(
|
|||
});
|
||||
|
||||
return options;
|
||||
|
||||
};
|
||||
|
||||
App.DI.register('fileDropper', App.Controls.FileDropper);
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
var App = App || {};
|
||||
App.Controls = App.Controls || {};
|
||||
|
||||
//todo: autocomplete
|
||||
|
||||
App.Controls.TagInput = function(
|
||||
$underlyingInput,
|
||||
_,
|
||||
jQuery) {
|
||||
App.Controls.TagInput = function($underlyingInput) {
|
||||
var _ = App.DI.get('_');
|
||||
var jQuery = App.DI.get('jQuery');
|
||||
|
||||
var KEY_RETURN = 13;
|
||||
var KEY_SPACE = 32;
|
||||
|
@ -48,21 +45,36 @@ App.Controls.TagInput = function(
|
|||
});
|
||||
$input.attr('placeholder', $underlyingInput.attr('placeholder'));
|
||||
|
||||
var tagsToAdd = $underlyingInput.val().split(/\s+/);
|
||||
_.map(tagsToAdd, addTag);
|
||||
addTagsFromText($underlyingInput.val());
|
||||
$underlyingInput.val('');
|
||||
|
||||
$input.unbind('focus').bind('focus', function(e) {
|
||||
initAutocomplete();
|
||||
|
||||
function initAutocomplete() {
|
||||
var autocomplete = new App.Controls.AutoCompleteInput($input);
|
||||
autocomplete.onApply = function(text) {
|
||||
addTagsFromText(text);
|
||||
$input.val('');
|
||||
};
|
||||
autocomplete.additionalFilter = function(results) {
|
||||
var tags = getTags();
|
||||
return _.filter(results, function(resultItem) {
|
||||
return !_.contains(tags, resultItem[0]);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
$input.bind('focus', function(e) {
|
||||
$wrapper.addClass('focused');
|
||||
});
|
||||
$input.unbind('blur').bind('blur', function(e) {
|
||||
$input.bind('blur', function(e) {
|
||||
$wrapper.removeClass('focused');
|
||||
var tag = $input.val();
|
||||
addTag(tag);
|
||||
$input.val('');
|
||||
});
|
||||
|
||||
$input.unbind('paste').bind('paste', function(e) {
|
||||
$input.bind('paste', function(e) {
|
||||
e.preventDefault();
|
||||
var pastedText;
|
||||
if (window.clipboardData) {
|
||||
|
@ -82,7 +94,7 @@ App.Controls.TagInput = function(
|
|||
$input.val(lastTag);
|
||||
});
|
||||
|
||||
$input.unbind('keydown').bind('keydown', function(e) {
|
||||
$input.bind('keydown', function(e) {
|
||||
if (_.contains(inputConfirmKeys, e.which) && !$input.val()) {
|
||||
e.preventDefault();
|
||||
if (typeof(options.inputConfirmed) !== 'undefined') {
|
||||
|
@ -99,6 +111,11 @@ App.Controls.TagInput = function(
|
|||
}
|
||||
});
|
||||
|
||||
function addTagsFromText(text) {
|
||||
var tagsToAdd = text.split(/\s+/);
|
||||
_.map(tagsToAdd, addTag);
|
||||
}
|
||||
|
||||
function addTag(tag) {
|
||||
tag = tag.trim();
|
||||
if (tag.length === 0) {
|
||||
|
@ -185,5 +202,3 @@ App.Controls.TagInput = function(
|
|||
});
|
||||
return options;
|
||||
};
|
||||
|
||||
App.DI.register('tagInput', App.Controls.TagInput);
|
||||
|
|
|
@ -70,6 +70,7 @@ App.Presenters.PostListPresenter = function(
|
|||
function render() {
|
||||
$el.html(templates.list());
|
||||
$searchInput = $el.find('input[name=query]');
|
||||
App.Controls.AutoCompleteInput($searchInput);
|
||||
|
||||
$searchInput.val(searchArgs.query);
|
||||
$searchInput.keydown(searchInputKeyPressed);
|
||||
|
@ -80,7 +81,7 @@ App.Presenters.PostListPresenter = function(
|
|||
});
|
||||
|
||||
keyboard.keyup('q', function() {
|
||||
$searchInput.eq(0).focus();
|
||||
$searchInput.eq(0).focus().select();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -95,7 +96,7 @@ App.Presenters.PostListPresenter = function(
|
|||
var $post = jQuery('<li>' + templates.listItem({
|
||||
searchArgs: searchArgs,
|
||||
post: post,
|
||||
}) + '</li>')
|
||||
}) + '</li>');
|
||||
util.loadImagesNicely($post.find('img'));
|
||||
$target.append($post);
|
||||
});
|
||||
|
@ -116,7 +117,7 @@ App.Presenters.PostListPresenter = function(
|
|||
function updateSearch() {
|
||||
$searchInput.blur();
|
||||
pagerPresenter.setSearchParams({
|
||||
query: $searchInput.val(),
|
||||
query: $searchInput.val().trim(),
|
||||
order: searchArgs.order});
|
||||
}
|
||||
|
||||
|
|
|
@ -135,14 +135,14 @@ App.Presenters.PostPresenter = function(
|
|||
$messages = $el.find('.messages');
|
||||
|
||||
if (editPrivileges.canChangeTags) {
|
||||
tagInput = App.Controls.TagInput($el.find('form [name=tags]'), _, jQuery);
|
||||
tagInput = new App.Controls.TagInput($el.find('form [name=tags]'));
|
||||
tagInput.inputConfirmed = editPost;
|
||||
}
|
||||
|
||||
postContentFileDropper = new App.Controls.FileDropper($el.find('form [name=content]'), _, jQuery);
|
||||
postContentFileDropper = new App.Controls.FileDropper($el.find('form [name=content]'));
|
||||
postContentFileDropper.onChange = postContentChanged;
|
||||
postContentFileDropper.setNames = true;
|
||||
postThumbnailFileDropper = new App.Controls.FileDropper($el.find('form [name=thumbnail]'), _, jQuery);
|
||||
postThumbnailFileDropper = new App.Controls.FileDropper($el.find('form [name=thumbnail]'));
|
||||
postThumbnailFileDropper.onChange = postThumbnailChanged;
|
||||
postThumbnailFileDropper.setNames = true;
|
||||
|
||||
|
|
|
@ -41,8 +41,8 @@ App.Presenters.PostUploadPresenter = function(
|
|||
}));
|
||||
$messages = $el.find('.messages');
|
||||
|
||||
tagInput = new App.Controls.TagInput($el.find('form [name=tags]'), _, jQuery);
|
||||
fileDropper = App.Controls.FileDropper($el.find('[name=post-content]'), _, jQuery);
|
||||
tagInput = new App.Controls.TagInput($el.find('form [name=tags]'));
|
||||
fileDropper = new App.Controls.FileDropper($el.find('[name=post-content]'));
|
||||
fileDropper.onChange = fileHandlerChanged;
|
||||
|
||||
$el.find('.url-handler input').keydown(urlHandlerKeyPressed);
|
||||
|
|
|
@ -54,7 +54,7 @@ App.Presenters.UserAccountSettingsPresenter = function(
|
|||
$el.find('form').submit(accountSettingsFormSubmitted);
|
||||
$el.find('form [name=avatar-style]').change(avatarStyleChanged);
|
||||
avatarStyleChanged();
|
||||
fileDropper = new App.Controls.FileDropper($el.find('[name=avatar-content]'), _, jQuery);
|
||||
fileDropper = new App.Controls.FileDropper($el.find('[name=avatar-content]'));
|
||||
fileDropper.onChange = avatarContentChanged;
|
||||
fileDropper.setNames = true;
|
||||
}
|
||||
|
|
27
public_html/js/Services/TagList.js
Normal file
27
public_html/js/Services/TagList.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
var App = App || {};
|
||||
App.Services = App.Services || {};
|
||||
|
||||
App.Services.TagList = function(jQuery) {
|
||||
var tags = [];
|
||||
|
||||
jQuery.ajax({
|
||||
success: function(data, textStatus, xhr) {
|
||||
tags = data;
|
||||
},
|
||||
error: function(xhr, textStatus, errorThrown) {
|
||||
console.log(new Error(errorThrown));
|
||||
},
|
||||
type: 'GET',
|
||||
url: '/data/tags.json',
|
||||
});
|
||||
|
||||
function getTags() {
|
||||
return tags;
|
||||
}
|
||||
|
||||
return {
|
||||
getTags: getTags,
|
||||
};
|
||||
};
|
||||
|
||||
App.DI.registerSingleton('tagList', ['jQuery'], App.Services.TagList);
|
Loading…
Reference in a new issue