diff --git a/CHANGELOG.md b/CHANGELOG.md index 99ea3606..d310bc17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Make burndown chart collapsible at the backlog panel. - Ability to choose a theme (thanks to [@astagi](https://github.com/astagi)) - Inline viewing of image attachments (thanks to [@brettp](https://github.com/brettp)). +- Autocomplete for usernames, user stories, tasks, issues, and wiki pages in text areas (thanks to [@brettp](https://github.com/brettp)). - i18n. - Add polish (pl) translation. - Add portuguese (Brazil) (pt_BR) translation. diff --git a/app/coffee/modules/common/wisiwyg.coffee b/app/coffee/modules/common/wisiwyg.coffee index cfd6a6bb..2918ffc1 100644 --- a/app/coffee/modules/common/wisiwyg.coffee +++ b/app/coffee/modules/common/wisiwyg.coffee @@ -181,7 +181,11 @@ MarkitupDirective = ($rootscope, $rs, $selectedText, $template, $compile, $trans onShiftEnter: {keepDefault:false, openWith:"\n\n"} onEnter: keepDefault: false, - replaceWith: () -> "\n" + replaceWith: () -> + # Allow textcomplete to intercept the enter key if the options list is displayed + # @todo There doesn't seem to be a more graceful way to do this with the textcomplete API. + if not $('.textcomplete-dropdown').is(':visible') + "\n" afterInsert: (data) -> lines = data.textarea.value.split("\n") # Detect if we are in this situation +- aa at the beginning if the textarea @@ -347,6 +351,104 @@ MarkitupDirective = ($rootscope, $rs, $selectedText, $template, $compile, $trans element .markItUpRemove() .markItUp(markdownSettings) + .textcomplete([ + # us, task, and issue autocomplete: #id or # + { + cache: true + match: /(^|\s)#([a-z0-9]+)$/i, + search: (term, callback) -> + term = taiga.slugify(term) + + searchTypes = ['issues', 'tasks', 'userstories'] + searchProps = ['ref', 'subject'] + + filter = (item) => + for prop in searchProps + if taiga.slugify(item[prop]).indexOf(term) >= 0 + return true + return false + + $rs.search.do($scope.projectId, term).then (res) => + # ignore wikipages if they're the only results. can't exclude them in search + if res.count < 1 or res.count == res.wikipages.length + callback([]) + + else + for type in searchTypes + if res[type] and res[type].length > 0 + callback(res[type].filter(filter), true) + + # must signal end of lists + callback([]) + + replace: (res) -> + return "$1\##{res.ref} " + + template: (res, term) -> + return "\##{res.ref} - #{res.subject}" + } + + # username autocomplete: @username or @ + { + cache: true + match: /(^|\s)@([a-z0-9\-\._]{2,})$/i + search: (term, callback) -> + username = taiga.slugify(term) + searchProps = ['username', 'full_name', 'full_name_display'] + + if $scope.project.members.length < 1 + callback([]) + + else + callback $scope.project.members.filter (user) => + for prop in searchProps + if taiga.slugify(user[prop]).indexOf(username) >= 0 + return true + return false + + replace: (user) -> + return "$1@#{user.username} " + + template: (user) -> + return "#{user.username} - #{user.full_name_display}" + } + + # wiki pages autocomplete: [[slug or [[ + # if the search function was called with the 3rd param the regex + # like the docs claim, we could combine this with the #123 search + { + cache: true + match: /(^|\s)\[\[([a-z0-9\-]+)$/i + search: (term, callback) -> + term = taiga.slugify(term) + + $rs.search.do($scope.projectId, term).then (res) => + if res.count < 1 + callback([]) + + if res.count < 1 or not res.wikipages or res.wikipages.length <= 0 + callback([]) + + else + callback res.wikipages.filter((page) => + return taiga.slugify(page['slug']).indexOf(term) >= 0 + ), true + + # must signal end of lists + callback([]) + + + replace: (res) -> + return "$1[[#{res.slug}]]" + + template: (res, term) -> + return res.slug + } + ], + { + debounce: 200 + } + ) renderMarkItUp() diff --git a/app/styles/vendor/jquery.textcomplete.css b/app/styles/vendor/jquery.textcomplete.css index ce74c742..978cc324 100644 --- a/app/styles/vendor/jquery.textcomplete.css +++ b/app/styles/vendor/jquery.textcomplete.css @@ -17,10 +17,12 @@ .dropdown-menu li:hover, .dropdown-menu .active { background-color: rgb(110, 183, 219); + color: white; } -.textcomplete-wrapper { - width: 100%; +.dropdown-menu .active > a, +.dropdown-menu .active > a:hover { + color: white; } /* SHOULD not modify */ diff --git a/bower.json b/bower.json index 7291fcec..be586d1e 100644 --- a/bower.json +++ b/bower.json @@ -62,7 +62,7 @@ "google-diff-match-patch-js": "~1.0.0", "underscore.string": "~2.3.3", "markitup-1x": "~1.1.14", - "jquery-textcomplete": "yuku-t/jquery-textcomplete#~0.1.1", + "jquery-textcomplete": "yuku-t/jquery-textcomplete#~0.7", "flot-orderBars": "emmerich/flot-orderBars", "flot-axislabels": "markrcote/flot-axislabels", "flot.tooltip": "~0.8.4", diff --git a/gulpfile.js b/gulpfile.js index c923045c..41c62d57 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -154,7 +154,7 @@ paths.libs = [ paths.vendor + "jquery-flot/jquery.flot.time.js", paths.vendor + "flot-axislabels/jquery.flot.axislabels.js", paths.vendor + "flot.tooltip/js/jquery.flot.tooltip.js", - paths.vendor + "jquery-textcomplete/jquery.textcomplete.js", + paths.vendor + "jquery-textcomplete/dist/jquery.textcomplete.js", paths.vendor + "markitup-1x/markitup/jquery.markitup.js", paths.vendor + "malihu-custom-scrollbar-plugin/jquery.mCustomScrollbar.concat.min.js", paths.vendor + "raven-js/dist/raven.js",