From 38d7234018ce48d3d900f05b07a68d889e030698 Mon Sep 17 00:00:00 2001 From: cpradio Date: Mon, 20 Feb 2017 21:49:24 -0500 Subject: [PATCH] FEATURE: Autocomplete support on advanced search --- .../components/search-advanced-options.js.es6 | 36 +++++++------- .../components/search-text-field.js.es6 | 47 +++++++++++++++++++ .../discourse/lib/autocomplete.js.es6 | 10 ++-- .../discourse/widgets/header.js.es6 | 42 ++++++++++++++++- 4 files changed, 113 insertions(+), 22 deletions(-) diff --git a/app/assets/javascripts/discourse/components/search-advanced-options.js.es6 b/app/assets/javascripts/discourse/components/search-advanced-options.js.es6 index 23bdac794b4..fe6db1a0fd2 100644 --- a/app/assets/javascripts/discourse/components/search-advanced-options.js.es6 +++ b/app/assets/javascripts/discourse/components/search-advanced-options.js.es6 @@ -3,24 +3,26 @@ import { escapeExpression } from 'discourse/lib/utilities'; const REGEXP_BLOCKS = /(([^" \t\n\x0B\f\r]+)?(("[^"]+")?))/g; -const REGEXP_USERNAME_PREFIX = /(user:|@)/ig; -const REGEXP_CATEGORY_PREFIX = /(category:|#)/ig; -const REGEXP_GROUP_PREFIX = /group:/ig; -const REGEXP_BADGE_PREFIX = /badge:/ig; -const REGEXP_TAGS_PREFIX = /tags?:/ig; -const REGEXP_IN_PREFIX = /in:/ig; -const REGEXP_STATUS_PREFIX = /status:/ig; -const REGEXP_MIN_POST_COUNT_PREFIX = /min_post_count:/ig; -const REGEXP_POST_TIME_PREFIX = /(before|after):/ig; +const REGEXP_USERNAME_PREFIX = /^(user:|@)/ig; +const REGEXP_CATEGORY_PREFIX = /^(category:|#)/ig; +const REGEXP_GROUP_PREFIX = /^group:/ig; +const REGEXP_BADGE_PREFIX = /^badge:/ig; +const REGEXP_TAGS_PREFIX = /^(tags?:|#(?=[a-z0-9\-]+::tag))/ig; +const REGEXP_IN_PREFIX = /^in:/ig; +const REGEXP_STATUS_PREFIX = /^status:/ig; +const REGEXP_MIN_POST_COUNT_PREFIX = /^min_post_count:/ig; +const REGEXP_POST_TIME_PREFIX = /^(before|after):/ig; +const REGEXP_TAGS_SUFFIX = /::tag\s?$/ig; -const REGEXP_IN_MATCH = /in:(posted|watching|tracking|bookmarks|first|pinned|unpinned)/ig; -const REGEXP_SPECIAL_IN_LIKES_MATCH = /in:likes/ig; -const REGEXP_SPECIAL_IN_PRIVATE_MATCH = /in:private/ig; -const REGEXP_SPECIAL_IN_WIKI_MATCH = /in:wiki/ig; -const REGEXP_CATEGORY_SLUG = /(\#[a-zA-Z0-9\-:]+)/ig; -const REGEXP_CATEGORY_ID = /(category:[0-9]+)/ig; -const REGEXP_POST_TIME_WHEN = /(before|after)/ig; +const REGEXP_IN_MATCH = /^in:(posted|watching|tracking|bookmarks|first|pinned|unpinned)/ig; +const REGEXP_SPECIAL_IN_LIKES_MATCH = /^in:likes/ig; +const REGEXP_SPECIAL_IN_PRIVATE_MATCH = /^in:private/ig; +const REGEXP_SPECIAL_IN_WIKI_MATCH = /^in:wiki/ig; + +const REGEXP_CATEGORY_SLUG = /^(\#[a-zA-Z0-9\-:]+)/ig; +const REGEXP_CATEGORY_ID = /^(category:[0-9]+)/ig; +const REGEXP_POST_TIME_WHEN = /^(before|after)/ig; export default Em.Component.extend({ classNames: ['search-advanced-options'], @@ -228,7 +230,7 @@ export default Em.Component.extend({ if (match.length !== 0) { const existingInput = _.isArray(tags) ? tags.join(',') : tags; - const userInput = match[0].replace(REGEXP_TAGS_PREFIX, ''); + const userInput = match[0].replace(REGEXP_TAGS_PREFIX, '').replace(REGEXP_TAGS_SUFFIX, ''); if (existingInput !== userInput) { this.set('searchedTerms.tags', (userInput.length !== 0) ? userInput.split(',') : []); diff --git a/app/assets/javascripts/discourse/components/search-text-field.js.es6 b/app/assets/javascripts/discourse/components/search-text-field.js.es6 index a086a51f32a..339545bf86d 100644 --- a/app/assets/javascripts/discourse/components/search-text-field.js.es6 +++ b/app/assets/javascripts/discourse/components/search-text-field.js.es6 @@ -1,6 +1,12 @@ import computed from 'ember-addons/ember-computed-decorators'; import { on } from 'ember-addons/ember-computed-decorators'; import TextField from 'discourse/components/text-field'; +import { findRawTemplate } from 'discourse/lib/raw-templates'; +import { TAG_HASHTAG_POSTFIX } from 'discourse/lib/tag-hashtags'; +import { SEPARATOR } from 'discourse/lib/category-hashtags'; +import Category from 'discourse/models/category'; +import { search as searchCategoryTag } from 'discourse/lib/category-tag-search'; +import userSearch from 'discourse/lib/user-search'; export default TextField.extend({ @computed('searchService.searchContextEnabled') @@ -15,5 +21,46 @@ export default TextField.extend({ // at the top of the page $(window).scrollTop(0); this.$().focus(); + }, + + @on("didInsertElement") + applyAutoComplete() { + this._super(); + + const $searchInput = this.$(); + this._applyCategoryHashtagAutocomplete($searchInput); + this._applyUsernameAutocomplete($searchInput); + }, + + _applyCategoryHashtagAutocomplete($searchInput) { + const siteSettings = this.siteSettings; + + $searchInput.autocomplete({ + template: findRawTemplate('category-tag-autocomplete'), + key: '#', + width: '100%', + treatAsTextarea: true, + transformComplete(obj) { + if (obj.model) { + return Category.slugFor(obj.model, SEPARATOR); + } else { + return `${obj.text}${TAG_HASHTAG_POSTFIX}`; + } + }, + dataSource(term) { + return searchCategoryTag(term, siteSettings); + } + }); + }, + + _applyUsernameAutocomplete($searchInput) { + $searchInput.autocomplete({ + template: findRawTemplate('user-selector-autocomplete'), + dataSource: term => userSearch({ term, undefined, includeGroups: true }), + key: "@", + width: '100%', + treatAsTextarea: true, + transformComplete: v => v.username || v.name + }); } }); diff --git a/app/assets/javascripts/discourse/lib/autocomplete.js.es6 b/app/assets/javascripts/discourse/lib/autocomplete.js.es6 index 5e1d404bc9d..c96ca3fb133 100644 --- a/app/assets/javascripts/discourse/lib/autocomplete.js.es6 +++ b/app/assets/javascripts/discourse/lib/autocomplete.js.es6 @@ -82,7 +82,7 @@ export default function(options) { let prevTerm = null; // input is handled differently - const isInput = this[0].tagName === "INPUT"; + const isInput = this[0].tagName === "INPUT" && !options.treatAsTextarea; let inputSelectedItems = []; function closeAutocomplete() { @@ -175,8 +175,10 @@ export default function(options) { wrap.width(width); } - if(options.single) { - this.css("width","100%"); + if(options.single && !options.width) { + this.css("width", "100%"); + } else if (options.width) { + this.css("width", options.width); } else { this.width(150); } @@ -258,7 +260,7 @@ export default function(options) { me.parent().append(div); - if (!isInput) { + if (!isInput && !options.treatAsTextarea) { vOffset = div.height(); if ((window.innerHeight - me.outerHeight() - $("header.d-header").innerHeight()) < vOffset) { diff --git a/app/assets/javascripts/discourse/widgets/header.js.es6 b/app/assets/javascripts/discourse/widgets/header.js.es6 index 252baf173e2..7f349ff3f22 100644 --- a/app/assets/javascripts/discourse/widgets/header.js.es6 +++ b/app/assets/javascripts/discourse/widgets/header.js.es6 @@ -3,6 +3,12 @@ import { iconNode } from 'discourse/helpers/fa-icon-node'; import { avatarImg } from 'discourse/widgets/post'; import DiscourseURL from 'discourse/lib/url'; import { wantsNewWindow } from 'discourse/lib/intercept-click'; +import { findRawTemplate } from 'discourse/lib/raw-templates'; +import { TAG_HASHTAG_POSTFIX } from 'discourse/lib/tag-hashtags'; +import { SEPARATOR } from 'discourse/lib/category-hashtags'; +import Category from 'discourse/models/category'; +import { search as searchCategoryTag } from 'discourse/lib/category-tag-search'; +import userSearch from 'discourse/lib/user-search'; import { h } from 'virtual-dom'; @@ -256,7 +262,11 @@ export default createWidget('header', { this.updateHighlight(); if (this.state.searchVisible) { - Ember.run.schedule('afterRender', () => $('#search-term').focus().select()); + Ember.run.schedule('afterRender', () => { + const $searchInput = $('#search-term'); + $searchInput.focus().select(); + this.applyAutocomplete($searchInput); + }); } }, @@ -340,6 +350,36 @@ export default createWidget('header', { return Ember.get(ctx, 'type'); } } + }, + + applyAutocomplete($searchInput) { + const siteSettings = this.siteSettings; + + $searchInput.autocomplete({ + template: findRawTemplate('category-tag-autocomplete'), + key: '#', + width: '100%', + treatAsTextarea: true, + transformComplete(obj) { + if (obj.model) { + return Category.slugFor(obj.model, SEPARATOR); + } else { + return `${obj.text}${TAG_HASHTAG_POSTFIX}`; + } + }, + dataSource(term) { + return searchCategoryTag(term, siteSettings); + } + }); + + $searchInput.autocomplete({ + template: findRawTemplate('user-selector-autocomplete'), + dataSource: term => userSearch({ term, undefined, includeGroups: true }), + key: "@", + width: '100%', + treatAsTextarea: true, + transformComplete: v => v.username || v.name + }); } });