FEATURE: Autocomplete support on advanced search
PERF: Extract autocomplete initialization to a function PERF: Create a REGEXP_TAGS_REPLACE regex to remove a chained .replace call FIX: autocomplete positioning FIX: Collapsing/Expanding Advanced Search doesn't wipe out Advanced Search Terms from search query. FIX: Populate Category when query/search term is updated FIX: Using enter to complete autocomplete doesn't automatically send you to full page search
This commit is contained in:
parent
1dda998a4e
commit
4c7a21c76e
|
@ -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_REPLACE = /(^(tags?:|#(?=[a-z0-9\-]+::tag))|::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'],
|
||||
|
@ -48,8 +50,8 @@ export default Em.Component.extend({
|
|||
|
||||
init() {
|
||||
this._super();
|
||||
Ember.run.scheduleOnce('afterRender', () => {
|
||||
this._init();
|
||||
Ember.run.scheduleOnce('afterRender', () => {
|
||||
this._update();
|
||||
});
|
||||
},
|
||||
|
@ -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_REPLACE, '');
|
||||
|
||||
if (existingInput !== userInput) {
|
||||
this.set('searchedTerms.tags', (userInput.length !== 0) ? userInput.split(',') : []);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import { on } from 'ember-addons/ember-computed-decorators';
|
||||
import TextField from 'discourse/components/text-field';
|
||||
import { applySearchAutocomplete } from "discourse/lib/search";
|
||||
|
||||
export default TextField.extend({
|
||||
@computed('searchService.searchContextEnabled')
|
||||
|
@ -10,10 +11,13 @@ export default TextField.extend({
|
|||
|
||||
@on("didInsertElement")
|
||||
becomeFocused() {
|
||||
const $searchInput = this.$();
|
||||
applySearchAutocomplete($searchInput, this.siteSettings);
|
||||
|
||||
if (!this.get('hasAutofocus')) { return; }
|
||||
// iOS is crazy, without this we will not be
|
||||
// at the top of the page
|
||||
$(window).scrollTop(0);
|
||||
this.$().focus();
|
||||
$searchInput.focus();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
if(options.single && !options.width) {
|
||||
this.css("width", "100%");
|
||||
} else if (options.width) {
|
||||
this.css("width", options.width);
|
||||
} else {
|
||||
this.width(150);
|
||||
}
|
||||
|
@ -245,6 +247,13 @@ export default function(options) {
|
|||
};
|
||||
vOffset = -32;
|
||||
hOffset = 0;
|
||||
} if (options.treatAsTextarea) {
|
||||
pos = me.caretPosition({
|
||||
pos: completeStart,
|
||||
key: options.key
|
||||
});
|
||||
hOffset = 27;
|
||||
vOffset = -32;
|
||||
} else {
|
||||
pos = me.caretPosition({
|
||||
pos: completeStart,
|
||||
|
@ -258,7 +267,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) {
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
import { ajax } from 'discourse/lib/ajax';
|
||||
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 function translateResults(results, opts) {
|
||||
|
||||
const User = require('discourse/models/user').default;
|
||||
const Category = require('discourse/models/category').default;
|
||||
const Post = require('discourse/models/post').default;
|
||||
const Topic = require('discourse/models/topic').default;
|
||||
|
||||
|
@ -124,3 +129,31 @@ export function isValidSearchTerm(searchTerm) {
|
|||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export function applySearchAutocomplete($input, siteSettings) {
|
||||
$input.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);
|
||||
}
|
||||
});
|
||||
|
||||
$input.autocomplete({
|
||||
template: findRawTemplate('user-selector-autocomplete'),
|
||||
dataSource: term => userSearch({ term, undefined, includeGroups: true }),
|
||||
key: "@",
|
||||
width: '100%',
|
||||
treatAsTextarea: true,
|
||||
transformComplete: v => v.username || v.name
|
||||
});
|
||||
};
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<div class="control-group pull-left">
|
||||
<label class="control-label" for="search-in-category">{{i18n "search.advanced.in_category.label"}}</label>
|
||||
<div class="controls">
|
||||
{{category-selector categories=searchedTerms.category single="true"}}
|
||||
{{category-selector categories=searchedTerms.category single="true" canReceiveUpdates="true"}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,6 +3,7 @@ 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 { applySearchAutocomplete } from "discourse/lib/search";
|
||||
|
||||
import { h } from 'virtual-dom';
|
||||
|
||||
|
@ -256,7 +257,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();
|
||||
applySearchAutocomplete($searchInput, this.siteSettings);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -5,6 +5,11 @@ import { createWidget } from 'discourse/widgets/widget';
|
|||
createWidget('search-term', {
|
||||
tagName: 'input',
|
||||
buildId: () => 'search-term',
|
||||
buildKey: (attrs) => `search-term-${attrs.id}`,
|
||||
|
||||
defaultState() {
|
||||
return { autocompleteIsOpen: false };
|
||||
},
|
||||
|
||||
buildAttributes(attrs) {
|
||||
return { type: 'text',
|
||||
|
@ -12,8 +17,17 @@ createWidget('search-term', {
|
|||
placeholder: attrs.contextEnabled ? "" : I18n.t('search.title') };
|
||||
},
|
||||
|
||||
keyDown(e) {
|
||||
const state = this.state;
|
||||
if ($(`#${this.buildId()}`).parent().find('.autocomplete').length !== 0) {
|
||||
state.autocompleteIsOpen = true;
|
||||
} else {
|
||||
state.autocompleteIsOpen = false;
|
||||
}
|
||||
},
|
||||
|
||||
keyUp(e) {
|
||||
if (e.which === 13) {
|
||||
if (e.which === 13 && !this.state.autocompleteIsOpen) {
|
||||
return this.sendWidgetAction('fullSearch');
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue