Migrate search drop down to `menu-panel` component.
This commit is contained in:
parent
3bc79f6885
commit
d4b987ff32
|
@ -1,13 +1,14 @@
|
|||
import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
const PANEL_BODY_MARGIN = 30;
|
||||
const mutationSupport = !!window['MutationObserver'];
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNameBindings: [':menu-panel', 'visible::hidden', 'viewMode'],
|
||||
|
||||
showClose: Ember.computed.equal('viewMode', 'slide-in'),
|
||||
|
||||
_resizeComponent() {
|
||||
_layoutComponent() {
|
||||
if (!this.get('visible')) { return; }
|
||||
|
||||
const viewMode = this.get('viewMode');
|
||||
|
@ -27,26 +28,22 @@ export default Ember.Component.extend({
|
|||
this.$().css({ left: posLeft + "px", top: posTop + "px" });
|
||||
|
||||
// adjust panel height
|
||||
let contentHeight = parseInt($('.panel-body-contents').height());
|
||||
let contentHeight = parseInt(this.$('.panel-body-contents').height());
|
||||
const fullHeight = parseInt($(window).height());
|
||||
|
||||
const offsetTop = this.$().offset().top;
|
||||
if (contentHeight + offsetTop + PANEL_BODY_MARGIN > fullHeight) {
|
||||
contentHeight = fullHeight - (offsetTop - $(window).scrollTop()) - PANEL_BODY_MARGIN;
|
||||
const scrollTop = $(window).scrollTop();
|
||||
if (contentHeight + (offsetTop - scrollTop) + PANEL_BODY_MARGIN > fullHeight) {
|
||||
contentHeight = fullHeight - (offsetTop - scrollTop) - PANEL_BODY_MARGIN;
|
||||
}
|
||||
$panelBody.height(contentHeight);
|
||||
} else {
|
||||
$panelBody.height('auto');
|
||||
|
||||
const headerHeight = parseInt($('header.d-header').height() + 3);
|
||||
this.$().css({ left: "auto", top: headerHeight + "px" });
|
||||
}
|
||||
},
|
||||
|
||||
_needsResize() {
|
||||
Ember.run.scheduleOnce('afterRender', this, this._resizeComponent);
|
||||
},
|
||||
|
||||
@computed('force')
|
||||
viewMode() {
|
||||
const force = this.get('force');
|
||||
|
@ -61,6 +58,10 @@ export default Ember.Component.extend({
|
|||
const markActive = this.get('markActive');
|
||||
|
||||
if (this.get('visible')) {
|
||||
this.appEvents.on('dropdowns:closeAll', this, this.hide);
|
||||
|
||||
// Allow us to hook into things being shown
|
||||
Ember.run.scheduleOnce('afterRender', () => this.sendAction('onVisible'));
|
||||
|
||||
if (isDropdown && markActive) {
|
||||
$(markActive).addClass('active');
|
||||
|
@ -72,14 +73,16 @@ export default Ember.Component.extend({
|
|||
if ($target.closest('.menu-panel').length > 0) { return; }
|
||||
this.hide();
|
||||
});
|
||||
|
||||
this.performLayout();
|
||||
this._watchSizeChanges();
|
||||
} else {
|
||||
Ember.run.scheduleOnce('afterRender', () => this.sendAction('onHidden'));
|
||||
if (markActive) {
|
||||
$(markActive).removeClass('active');
|
||||
}
|
||||
$('html').off('click.close-menu-panel');
|
||||
this._stopWatchingSize();
|
||||
}
|
||||
this._needsResize();
|
||||
},
|
||||
|
||||
@computed()
|
||||
|
@ -102,9 +105,38 @@ export default Ember.Component.extend({
|
|||
return this.siteSettings.faq_url ? this.siteSettings.faq_url : Discourse.getURL('/faq');
|
||||
},
|
||||
|
||||
performLayout() {
|
||||
Ember.run.scheduleOnce('afterRender', this, this._layoutComponent);
|
||||
},
|
||||
|
||||
_watchSizeChanges() {
|
||||
if (mutationSupport) {
|
||||
this._observer.disconnect();
|
||||
this._observer.observe(this.element, { childList: true, subtree: true });
|
||||
} else {
|
||||
clearInterval(this._resizeInterval);
|
||||
this._resizeInterval = setInterval(() => {
|
||||
Ember.run(() => {
|
||||
const contentHeight = parseInt(this.$('.panel-body-contents').height());
|
||||
if (contentHeight !== this._lastHeight) { this.performLayout(); }
|
||||
this._lastHeight = contentHeight;
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
},
|
||||
|
||||
_stopWatchingSize() {
|
||||
if (mutationSupport) {
|
||||
this._observer.disconnect();
|
||||
} else {
|
||||
clearInterval(this._resizeInterval);
|
||||
}
|
||||
},
|
||||
|
||||
@on('didInsertElement')
|
||||
_bindEvents() {
|
||||
this.$().on('click.discourse-menu-panel', 'a', () => {
|
||||
this.$().on('click.discourse-menu-panel', 'a', (e) => {
|
||||
if ($(e.target).data('ember-action')) { return; }
|
||||
this.hide();
|
||||
});
|
||||
|
||||
|
@ -116,11 +148,16 @@ export default Ember.Component.extend({
|
|||
}
|
||||
});
|
||||
|
||||
// Recompute styles on resize
|
||||
$(window).on('resize.discourse-menu-panel', () => {
|
||||
this.propertyDidChange('viewMode');
|
||||
this._needsResize();
|
||||
this.performLayout();
|
||||
});
|
||||
|
||||
if (mutationSupport) {
|
||||
this._observer = new MutationObserver(() => {
|
||||
Ember.run(() => this.performLayout());
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@on('willDestroyElement')
|
||||
|
|
|
@ -0,0 +1,174 @@
|
|||
import searchForTerm from 'discourse/lib/search-for-term';
|
||||
import DiscourseURL from 'discourse/lib/url';
|
||||
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
|
||||
import showModal from 'discourse/lib/show-modal';
|
||||
|
||||
let _dontSearch = false;
|
||||
export default Ember.Component.extend({
|
||||
searchService: Ember.inject.service('search'),
|
||||
classNames: ['search-menu'],
|
||||
typeFilter: null,
|
||||
|
||||
@observes('searchService.searchContext')
|
||||
contextChanged: function() {
|
||||
if (this.get('searchService.searchContextEnabled')) {
|
||||
_dontSearch = true;
|
||||
this.set('searchService.searchContextEnabled', false);
|
||||
_dontSearch = false;
|
||||
}
|
||||
},
|
||||
|
||||
@computed('searchService.searchContext', 'searchService.term', 'searchService.searchContextEnabled')
|
||||
fullSearchUrlRelative(searchContext, term, searchContextEnabled) {
|
||||
|
||||
if (searchContextEnabled && Ember.get(searchContext, 'type') === 'topic') {
|
||||
return null;
|
||||
}
|
||||
|
||||
let url = '/search?q=' + encodeURIComponent(this.get('searchService.term'));
|
||||
if (searchContextEnabled) {
|
||||
if (searchContext.id.toString().toLowerCase() === this.get('currentUser.username_lower') &&
|
||||
searchContext.type === "private_messages"
|
||||
) {
|
||||
url += ' in:private';
|
||||
} else {
|
||||
url += encodeURIComponent(" " + searchContext.type + ":" + searchContext.id);
|
||||
}
|
||||
}
|
||||
|
||||
return url;
|
||||
},
|
||||
|
||||
@computed('fullSearchUrlRelative')
|
||||
fullSearchUrl(fullSearchUrlRelative) {
|
||||
if (fullSearchUrlRelative) {
|
||||
return Discourse.getURL(fullSearchUrlRelative);
|
||||
}
|
||||
},
|
||||
|
||||
@computed('searchService.searchContext')
|
||||
searchContextDescription(ctx) {
|
||||
if (ctx) {
|
||||
switch(Em.get(ctx, 'type')) {
|
||||
case 'topic':
|
||||
return I18n.t('search.context.topic');
|
||||
case 'user':
|
||||
return I18n.t('search.context.user', {username: Em.get(ctx, 'user.username')});
|
||||
case 'category':
|
||||
return I18n.t('search.context.category', {category: Em.get(ctx, 'category.name')});
|
||||
case 'private_messages':
|
||||
return I18n.t('search.context.private_messages');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@observes('searchService.searchContextEnabled')
|
||||
searchContextEnabledChanged() {
|
||||
if (_dontSearch) { return; }
|
||||
this.newSearchNeeded();
|
||||
},
|
||||
|
||||
// If we need to perform another search
|
||||
@observes('searchService.term', 'typeFilter')
|
||||
newSearchNeeded() {
|
||||
this.set('noResults', false);
|
||||
const term = (this.get('searchService.term') || '').trim();
|
||||
if (term.length >= Discourse.SiteSettings.min_search_term_length) {
|
||||
this.set('loading', true);
|
||||
Ember.run.debounce(this, 'searchTerm', term, this.get('typeFilter'), 400);
|
||||
} else {
|
||||
this.setProperties({ content: null });
|
||||
}
|
||||
this.set('selectedIndex', 0);
|
||||
},
|
||||
|
||||
searchTerm(term, typeFilter) {
|
||||
// for cancelling debounced search
|
||||
if (this._cancelSearch){
|
||||
this._cancelSearch = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._search) {
|
||||
this._search.abort();
|
||||
}
|
||||
|
||||
const searchContext = this.get('searchService.searchContextEnabled') ? this.get('searchService.searchContext') : null;
|
||||
this._search = searchForTerm(term, { typeFilter, searchContext, fullSearchUrl: this.get('fullSearchUrl') });
|
||||
|
||||
this._search.then((content) => {
|
||||
this.setProperties({ noResults: !content, content });
|
||||
}).finally(() => {
|
||||
this.set('loading', false);
|
||||
this._search = null;
|
||||
});
|
||||
},
|
||||
|
||||
@computed('typeFilter', 'loading')
|
||||
showCancelFilter(typeFilter, loading) {
|
||||
if (loading) { return false; }
|
||||
return !Ember.isEmpty(typeFilter);
|
||||
},
|
||||
|
||||
@observes('searchService.term')
|
||||
termChanged() {
|
||||
this.cancelTypeFilter();
|
||||
},
|
||||
|
||||
actions: {
|
||||
fullSearch() {
|
||||
const self = this;
|
||||
|
||||
if (this._search) {
|
||||
this._search.abort();
|
||||
}
|
||||
|
||||
// maybe we are debounced and delayed
|
||||
// stop that as well
|
||||
this._cancelSearch = true;
|
||||
Em.run.later(function() {
|
||||
self._cancelSearch = false;
|
||||
}, 400);
|
||||
|
||||
const url = this.get('fullSearchUrlRelative');
|
||||
if (url) {
|
||||
DiscourseURL.routeTo(url);
|
||||
}
|
||||
},
|
||||
|
||||
moreOfType(type) {
|
||||
this.set('typeFilter', type);
|
||||
},
|
||||
|
||||
cancelType() {
|
||||
this.cancelTypeFilter();
|
||||
},
|
||||
|
||||
showedSearch() {
|
||||
$('#search-term').focus();
|
||||
},
|
||||
|
||||
showSearchHelp() {
|
||||
// TODO: @EvitTrout how do we get a loading indicator here?
|
||||
Discourse.ajax("/static/search_help.html", { dataType: 'html' }).then((model) => {
|
||||
showModal('searchHelp', { model });
|
||||
});
|
||||
},
|
||||
|
||||
cancelHighlight() {
|
||||
this.set('searchService.highlightTerm', null);
|
||||
}
|
||||
},
|
||||
|
||||
cancelTypeFilter() {
|
||||
this.set('typeFilter', null);
|
||||
},
|
||||
|
||||
keyDown(e) {
|
||||
const term = this.get('searchService.term');
|
||||
if (e.which === 13 && term && term.length >= this.siteSettings.min_search_term_length) {
|
||||
this.set('searchVisible', false);
|
||||
this.send('fullSearch');
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,7 +1,9 @@
|
|||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import TextField from 'discourse/components/text-field';
|
||||
|
||||
export default TextField.extend({
|
||||
placeholder: function() {
|
||||
return this.get('searchContextEnabled') ? "" : I18n.t('search.title');
|
||||
}.property('searchContextEnabled')
|
||||
@computed('searchService.searchContextEnabled')
|
||||
placeholder: function(searchContextEnabled) {
|
||||
return searchContextEnabled ? "" : I18n.t('search.title');
|
||||
}
|
||||
});
|
||||
|
|
|
@ -15,5 +15,5 @@ export default Ember.Controller.extend({
|
|||
@computed
|
||||
loginRequired() {
|
||||
return Discourse.SiteSettings.login_required && !Discourse.User.current();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -4,6 +4,7 @@ const HeaderController = Ember.Controller.extend({
|
|||
notifications: null,
|
||||
loadingNotifications: false,
|
||||
hamburgerVisible: false,
|
||||
searchVisible: false,
|
||||
needs: ['application'],
|
||||
|
||||
loginRequired: Em.computed.alias('controllers.application.loginRequired'),
|
||||
|
@ -71,9 +72,16 @@ const HeaderController = Ember.Controller.extend({
|
|||
headerView.showDropdownBySelector("#user-notifications");
|
||||
},
|
||||
|
||||
toggleSearchMenu() {
|
||||
this.appEvents.trigger('dropdowns:closeAll');
|
||||
this.toggleProperty('searchVisible');
|
||||
},
|
||||
|
||||
toggleHamburgerMenu() {
|
||||
this.appEvents.trigger('dropdowns:closeAll');
|
||||
this.toggleProperty('hamburgerVisible');
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -1,170 +0,0 @@
|
|||
import searchForTerm from 'discourse/lib/search-for-term';
|
||||
import DiscourseURL from 'discourse/lib/url';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
let _dontSearch = false;
|
||||
|
||||
export default Em.Controller.extend({
|
||||
typeFilter: null,
|
||||
|
||||
@computed('searchContext')
|
||||
contextType: {
|
||||
get(searchContext) {
|
||||
if (searchContext) {
|
||||
return Ember.get(searchContext, 'type');
|
||||
}
|
||||
},
|
||||
set(value, searchContext) {
|
||||
// a bit hacky, consider cleaning this up, need to work through all observers though
|
||||
const context = $.extend({}, searchContext);
|
||||
context.type = value;
|
||||
this.set('searchContext', context);
|
||||
return this.get('searchContext.type');
|
||||
}
|
||||
},
|
||||
|
||||
contextChanged: function(){
|
||||
if (this.get('searchContextEnabled')) {
|
||||
_dontSearch = true;
|
||||
this.set('searchContextEnabled', false);
|
||||
_dontSearch = false;
|
||||
}
|
||||
}.observes('searchContext'),
|
||||
|
||||
fullSearchUrlRelative: function(){
|
||||
|
||||
if (this.get('searchContextEnabled') && this.get('searchContext.type') === 'topic') {
|
||||
return null;
|
||||
}
|
||||
|
||||
let url = '/search?q=' + encodeURIComponent(this.get('term'));
|
||||
const searchContext = this.get('searchContext');
|
||||
|
||||
if (this.get('searchContextEnabled')) {
|
||||
if (searchContext.id.toString().toLowerCase() === this.get('currentUser.username_lower') &&
|
||||
searchContext.type === "private_messages"
|
||||
) {
|
||||
url += ' in:private';
|
||||
} else {
|
||||
url += encodeURIComponent(" " + searchContext.type + ":" + searchContext.id);
|
||||
}
|
||||
}
|
||||
|
||||
return url;
|
||||
|
||||
}.property('searchContext','term','searchContextEnabled'),
|
||||
|
||||
fullSearchUrl: function(){
|
||||
const url = this.get('fullSearchUrlRelative');
|
||||
if (url) {
|
||||
return Discourse.getURL(url);
|
||||
}
|
||||
}.property('fullSearchUrlRelative'),
|
||||
|
||||
searchContextDescription: function(){
|
||||
const ctx = this.get('searchContext');
|
||||
if (ctx) {
|
||||
switch(Em.get(ctx, 'type')) {
|
||||
case 'topic':
|
||||
return I18n.t('search.context.topic');
|
||||
case 'user':
|
||||
return I18n.t('search.context.user', {username: Em.get(ctx, 'user.username')});
|
||||
case 'category':
|
||||
return I18n.t('search.context.category', {category: Em.get(ctx, 'category.name')});
|
||||
case 'private_messages':
|
||||
return I18n.t('search.context.private_messages');
|
||||
}
|
||||
}
|
||||
}.property('searchContext'),
|
||||
|
||||
searchContextEnabledChanged: function(){
|
||||
if (_dontSearch) { return; }
|
||||
this.newSearchNeeded();
|
||||
}.observes('searchContextEnabled'),
|
||||
|
||||
// If we need to perform another search
|
||||
newSearchNeeded: function() {
|
||||
this.set('noResults', false);
|
||||
const term = (this.get('term') || '').trim();
|
||||
if (term.length >= Discourse.SiteSettings.min_search_term_length) {
|
||||
this.set('loading', true);
|
||||
|
||||
Ember.run.debounce(this, 'searchTerm', term, this.get('typeFilter'), 400);
|
||||
} else {
|
||||
this.setProperties({ content: null });
|
||||
}
|
||||
this.set('selectedIndex', 0);
|
||||
}.observes('term', 'typeFilter'),
|
||||
|
||||
searchTerm(term, typeFilter) {
|
||||
const self = this;
|
||||
|
||||
// for cancelling debounced search
|
||||
if (this._cancelSearch){
|
||||
this._cancelSearch = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._search) {
|
||||
this._search.abort();
|
||||
}
|
||||
|
||||
const searchContext = this.get('searchContextEnabled') ? this.get('searchContext') : null;
|
||||
|
||||
this._search = searchForTerm(term, {
|
||||
typeFilter,
|
||||
searchContext,
|
||||
fullSearchUrl: this.get('fullSearchUrl')
|
||||
});
|
||||
|
||||
this._search.then(function(results) {
|
||||
self.setProperties({ noResults: !results, content: results });
|
||||
}).finally(function() {
|
||||
self.set('loading', false);
|
||||
self._search = null;
|
||||
});
|
||||
},
|
||||
|
||||
showCancelFilter: function() {
|
||||
if (this.get('loading')) return false;
|
||||
return !Ember.isEmpty(this.get('typeFilter'));
|
||||
}.property('typeFilter', 'loading'),
|
||||
|
||||
termChanged: function() {
|
||||
this.cancelTypeFilter();
|
||||
}.observes('term'),
|
||||
|
||||
actions: {
|
||||
fullSearch() {
|
||||
const self = this;
|
||||
|
||||
if (this._search) {
|
||||
this._search.abort();
|
||||
}
|
||||
|
||||
// maybe we are debounced and delayed
|
||||
// stop that as well
|
||||
this._cancelSearch = true;
|
||||
Em.run.later(function(){
|
||||
self._cancelSearch = false;
|
||||
}, 400);
|
||||
|
||||
const url = this.get('fullSearchUrlRelative');
|
||||
if (url) {
|
||||
DiscourseURL.routeTo(url);
|
||||
}
|
||||
},
|
||||
|
||||
moreOfType(type) {
|
||||
this.set('typeFilter', type);
|
||||
},
|
||||
|
||||
cancelType() {
|
||||
this.cancelTypeFilter();
|
||||
}
|
||||
},
|
||||
|
||||
cancelTypeFilter() {
|
||||
this.set('typeFilter', null);
|
||||
}
|
||||
});
|
|
@ -8,14 +8,13 @@ import { popupAjaxError } from 'discourse/lib/ajax-error';
|
|||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
needs: ['header', 'modal', 'composer', 'quote-button', 'topic-progress', 'application'],
|
||||
multiSelect: false,
|
||||
needs: ['header', 'modal', 'composer', 'quote-button', 'search', 'topic-progress', 'application'],
|
||||
allPostsSelected: false,
|
||||
editingTopic: false,
|
||||
selectedPosts: null,
|
||||
selectedReplies: null,
|
||||
queryParams: ['filter', 'username_filters', 'show_deleted'],
|
||||
searchHighlight: null,
|
||||
loadedAllPosts: false,
|
||||
enteredAt: null,
|
||||
firstPostExpanded: false,
|
||||
|
@ -23,10 +22,6 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
|||
|
||||
maxTitleLength: setting('max_topic_title_length'),
|
||||
|
||||
contextChanged: function() {
|
||||
this.set('controllers.search.searchContext', this.get('model.searchContext'));
|
||||
}.observes('topic'),
|
||||
|
||||
_titleChanged: function() {
|
||||
const title = this.get('model.title');
|
||||
if (!Ember.isEmpty(title)) {
|
||||
|
@ -37,20 +32,6 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
|||
}
|
||||
}.observes('model.title', 'category'),
|
||||
|
||||
termChanged: function() {
|
||||
const dropdown = this.get('controllers.header.visibleDropdown');
|
||||
const term = this.get('controllers.search.term');
|
||||
|
||||
if(dropdown === 'search-dropdown' && term){
|
||||
this.set('searchHighlight', term);
|
||||
} else {
|
||||
if(this.get('searchHighlight')){
|
||||
this.set('searchHighlight', null);
|
||||
}
|
||||
}
|
||||
|
||||
}.observes('controllers.search.term', 'controllers.header.visibleDropdown'),
|
||||
|
||||
postStreamLoadedAllPostsChanged: function() {
|
||||
// semantics of loaded all posts are slightly diff at topic level,
|
||||
// it just means that we "once" loaded all posts, this means we don't
|
||||
|
|
|
@ -3,6 +3,7 @@ import KeyboardShortcuts from 'discourse/lib/keyboard-shortcuts';
|
|||
|
||||
export default {
|
||||
name: "keyboard-shortcuts",
|
||||
|
||||
initialize(container) {
|
||||
KeyboardShortcuts.bindEvents(Mousetrap, container);
|
||||
}
|
||||
|
|
|
@ -63,6 +63,9 @@ export default {
|
|||
this.container = container;
|
||||
this._stopCallback();
|
||||
|
||||
|
||||
this.searchService = this.container.lookup('search-service:main');
|
||||
|
||||
_.each(PATH_BINDINGS, this._bindToPath, this);
|
||||
_.each(CLICK_BINDINGS, this._bindToClick, this);
|
||||
_.each(SELECTED_POST_BINDINGS, this._bindToSelectedPost, this);
|
||||
|
@ -131,10 +134,7 @@ export default {
|
|||
},
|
||||
|
||||
showBuiltinSearch() {
|
||||
if ($('#search-dropdown').is(':visible')) {
|
||||
this._toggleSearch(false);
|
||||
return true;
|
||||
}
|
||||
this.searchService.set('searchContextEnabled', false);
|
||||
|
||||
const currentPath = this.container.lookup('controller:application').get('currentPath'),
|
||||
blacklist = [ /^discovery\.categories/ ],
|
||||
|
@ -144,11 +144,12 @@ export default {
|
|||
|
||||
// If we're viewing a topic, only intercept search if there are cloaked posts
|
||||
if (showSearch && currentPath.match(/^topic\./)) {
|
||||
showSearch = $('.cooked').length < this.container.lookup('controller:topic').get('postStream.stream.length');
|
||||
showSearch = $('.cooked').length < this.container.lookup('controller:topic').get('model.postStream.stream.length');
|
||||
}
|
||||
|
||||
if (showSearch) {
|
||||
this._toggleSearch(true);
|
||||
this.searchService.set('searchContextEnabled', true);
|
||||
this.showSearch();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -168,8 +169,7 @@ export default {
|
|||
},
|
||||
|
||||
showSearch() {
|
||||
this._toggleSearch(false);
|
||||
return false;
|
||||
this.container.lookup('controller:header').send('toggleSearchMenu');
|
||||
},
|
||||
|
||||
toggleHamburgerMenu() {
|
||||
|
@ -370,12 +370,5 @@ export default {
|
|||
|
||||
return oldStopCallback(e, element, combo);
|
||||
};
|
||||
},
|
||||
|
||||
_toggleSearch(selectContext) {
|
||||
$('#search-button').click();
|
||||
if (selectContext) {
|
||||
this.container.lookup('controller:search').set('searchContextEnabled', true);
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
|
@ -3,6 +3,7 @@ import AppEvents from 'discourse/lib/app-events';
|
|||
import Store from 'discourse/models/store';
|
||||
import DiscourseURL from 'discourse/lib/url';
|
||||
import DiscourseLocation from 'discourse/lib/discourse-location';
|
||||
import SearchService from 'discourse/services/search';
|
||||
|
||||
function inject() {
|
||||
const app = arguments[0],
|
||||
|
@ -35,6 +36,9 @@ export default {
|
|||
app.register('site-settings:main', Discourse.SiteSettings, { instantiate: false });
|
||||
injectAll(app, 'siteSettings');
|
||||
|
||||
app.register('search-service:main', SearchService);
|
||||
injectAll(app, 'searchService');
|
||||
|
||||
app.register('session:main', Session.current(), { instantiate: false });
|
||||
injectAll(app, 'session');
|
||||
|
||||
|
|
|
@ -99,13 +99,6 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, {
|
|||
showModal('keyboard-shortcuts-help', { title: 'keyboard_shortcuts_help.title'});
|
||||
},
|
||||
|
||||
showSearchHelp() {
|
||||
// TODO: @EvitTrout how do we get a loading indicator here?
|
||||
Discourse.ajax("/static/search_help.html", { dataType: 'html' }).then(function(model){
|
||||
showModal('searchHelp', { model });
|
||||
});
|
||||
},
|
||||
|
||||
// Close the current modal, and destroy its state.
|
||||
closeModal() {
|
||||
this.render('hide-modal', { into: 'modal', outlet: 'modalBody' });
|
||||
|
|
|
@ -81,7 +81,7 @@ export default function(filter, params) {
|
|||
expandAllPinned: true
|
||||
});
|
||||
|
||||
this.controllerFor('search').set('searchContext', model.get('searchContext'));
|
||||
this.searchService.set('searchContext', model.get('searchContext'));
|
||||
this.set('topics', null);
|
||||
|
||||
this.openTopicDraft(topics);
|
||||
|
@ -98,7 +98,7 @@ export default function(filter, params) {
|
|||
|
||||
deactivate: function() {
|
||||
this._super();
|
||||
this.controllerFor('search').set('searchContext', null);
|
||||
this.searchService.set('searchContext', null);
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
|
|
@ -25,11 +25,11 @@ export default (viewName, path) => {
|
|||
});
|
||||
|
||||
this.controllerFor("user").set("pmView", viewName);
|
||||
this.controllerFor("search").set("contextType", "private_messages");
|
||||
this.searchService.set('contextType', 'private_messages');
|
||||
},
|
||||
|
||||
deactivate() {
|
||||
this.controllerFor("search").set("contextType", "user");
|
||||
this.searchService.set('contextType', 'private_messages');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -175,14 +175,12 @@ const TopicRoute = Discourse.Route.extend({
|
|||
|
||||
const topic = this.modelFor('topic');
|
||||
this.session.set('lastTopicIdViewed', parseInt(topic.get('id'), 10));
|
||||
this.controllerFor('search').set('searchContext', topic.get('searchContext'));
|
||||
},
|
||||
|
||||
deactivate() {
|
||||
this._super();
|
||||
|
||||
// Clear the search context
|
||||
this.controllerFor('search').set('searchContext', null);
|
||||
this.searchService.set('searchContext', null);
|
||||
this.controllerFor('user-card').set('visible', false);
|
||||
|
||||
const topicController = this.controllerFor('topic'),
|
||||
|
@ -220,11 +218,8 @@ const TopicRoute = Discourse.Route.extend({
|
|||
|
||||
Discourse.TopicRoute.trigger('setupTopicController', this);
|
||||
|
||||
this.controllerFor('header').setProperties({
|
||||
topic: model,
|
||||
showExtraInfo: false
|
||||
});
|
||||
|
||||
this.controllerFor('header').setProperties({ topic: model, showExtraInfo: false });
|
||||
this.searchService.set('searchContext', model.get('searchContext'));
|
||||
this.controllerFor('topic-admin-menu').set('model', model);
|
||||
|
||||
this.controllerFor('composer').set('topic', model);
|
||||
|
|
|
@ -65,9 +65,7 @@ export default Discourse.Route.extend({
|
|||
|
||||
setupController(controller, user) {
|
||||
controller.set('model', user);
|
||||
|
||||
// Add a search context
|
||||
this.controllerFor('search').set('searchContext', user.get('searchContext'));
|
||||
this.searchService.set('searchContext', user.get('searchContext'))
|
||||
},
|
||||
|
||||
activate() {
|
||||
|
@ -83,7 +81,7 @@ export default Discourse.Route.extend({
|
|||
this.messageBus.unsubscribe("/users/" + this.modelFor('user').get('username_lower'));
|
||||
|
||||
// Remove the search context
|
||||
this.controllerFor('search').set('searchContext', null);
|
||||
this.searchService.set('searchContext', null)
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Object.extend({
|
||||
searchContextEnabled: false,
|
||||
searchContext: null,
|
||||
term: null,
|
||||
highlightTerm: null,
|
||||
|
||||
@observes('term')
|
||||
_sethighlightTerm() {
|
||||
this.set('highlightTerm', this.get('term'));
|
||||
},
|
||||
|
||||
@computed('searchContext')
|
||||
contextType: {
|
||||
get(searchContext) {
|
||||
if (searchContext) {
|
||||
return Ember.get(searchContext, 'type');
|
||||
}
|
||||
},
|
||||
set(value, searchContext) {
|
||||
// a bit hacky, consider cleaning this up, need to work through all observers though
|
||||
const context = $.extend({}, searchContext);
|
||||
context.type = value;
|
||||
this.set('searchContext', context);
|
||||
return this.get('searchContext.type');
|
||||
}
|
||||
},
|
||||
|
||||
});
|
|
@ -0,0 +1,44 @@
|
|||
{{#menu-panel visible=searchVisible
|
||||
markActive=".search-dropdown"
|
||||
onVisible="showedSearch"
|
||||
onHidden="cancelHighlight"}}
|
||||
|
||||
{{plugin-outlet "above-search"}}
|
||||
{{search-text-field value=searchService.term id="search-term"}}
|
||||
|
||||
<div class="search-context">
|
||||
{{#if searchService.searchContext}}
|
||||
<label>
|
||||
{{input type="checkbox" name="searchContext" checked=searchService.searchContextEnabled}} {{searchContextDescription}}
|
||||
</label>
|
||||
{{/if}}
|
||||
<a href class="show-help" {{action "showSearchHelp" bubbles=false}}>{{i18n "show_help"}}</a>
|
||||
<div class='clearfix'></div>
|
||||
</div>
|
||||
{{#if loading}}
|
||||
<div class="searching">{{loading-spinner}}</div>
|
||||
{{else}}
|
||||
<div class="results">
|
||||
{{#if noResults}}
|
||||
<div class="no-results">
|
||||
{{i18n "search.no_results"}}
|
||||
</div>
|
||||
{{else}}
|
||||
{{#each content.resultTypes as |resultType|}}
|
||||
<ul>
|
||||
<li class="heading row">{{resultType.name}}</li>
|
||||
{{component resultType.componentName results=resultType.results term=searchService.term}}
|
||||
</ul>
|
||||
<div class="no-results">
|
||||
{{#if resultType.moreUrl}}
|
||||
<a href={{resultType.moreUrl}} class="filter">{{i18n "show_more"}} {{fa-icon "chevron-down"}}</a>
|
||||
{{/if}}
|
||||
{{#if resultType.more}}
|
||||
<a href class="filter filter-type" {{action "moreOfType" resultType.type bubbles=false}}>{{i18n "show_more"}} {{fa-icon "chevron-down"}}</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/menu-panel}}
|
|
@ -1,4 +1,5 @@
|
|||
{{hamburger-menu hamburgerVisible=hamburgerVisible showKeyboardAction="showKeyboardShortcutsHelp"}}
|
||||
{{search-menu searchVisible=searchVisible}}
|
||||
|
||||
<div class='wrap'>
|
||||
<div class='contents clearfix'>
|
||||
|
@ -28,17 +29,15 @@
|
|||
{{/if}}
|
||||
</li>
|
||||
{{/if}}
|
||||
<li>
|
||||
<li class="search-dropdown">
|
||||
{{#if loginRequired}}
|
||||
<a id='search-button' class='icon expand' href='#' aria-hidden="true" {{action "showLogin"}}>
|
||||
<a id='search-button' class='icon expand' href aria-hidden="true" {{action "showLogin"}}>
|
||||
{{fa-icon "search"}}
|
||||
</a>
|
||||
{{else}}
|
||||
<a id='search-button'
|
||||
class='icon expand'
|
||||
href='#'
|
||||
data-dropdown="search-dropdown"
|
||||
title='{{i18n 'search.title'}}'>
|
||||
<a {{action "toggleSearchMenu"}} class='icon' href
|
||||
id='search-button'
|
||||
title={{i18n 'search.title'}}>
|
||||
{{fa-icon "search" label="search.title"}}
|
||||
</a>
|
||||
{{/if}}
|
||||
|
@ -81,7 +80,6 @@
|
|||
|
||||
{{#if view.renderDropdowns}}
|
||||
{{plugin-outlet "header-before-dropdowns"}}
|
||||
{{render "search"}}
|
||||
{{render "notifications" notifications}}
|
||||
{{render "user-dropdown"}}
|
||||
{{/if}}
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
{{plugin-outlet "above-search"}}
|
||||
{{search-text-field value=term searchContextEnabled=searchContextEnabled id="search-term"}}
|
||||
|
||||
<div class="search-context">
|
||||
{{#if searchContext}}
|
||||
<label>
|
||||
{{input type="checkbox" name="searchContext" checked=searchContextEnabled}} {{searchContextDescription}}
|
||||
</label>
|
||||
{{/if}}
|
||||
<a href class="show-help" {{action "showSearchHelp" bubbles=false}}>{{i18n "show_help"}}</a>
|
||||
</div>
|
||||
{{#if loading}}
|
||||
<div class="searching">{{loading-spinner}}</div>
|
||||
{{else}}
|
||||
<div class="results">
|
||||
{{#if noResults}}
|
||||
<div class="no-results">
|
||||
{{i18n "search.no_results"}}
|
||||
</div>
|
||||
{{else}}
|
||||
{{#each content.resultTypes as |resultType|}}
|
||||
<ul>
|
||||
<li class="heading row">{{resultType.name}}</li>
|
||||
{{component resultType.componentName results=resultType.results term=term}}
|
||||
</ul>
|
||||
<div class="no-results">
|
||||
{{#if resultType.moreUrl}}
|
||||
<a href="{{resultType.moreUrl}}" class="filter">{{i18n "show_more"}} {{fa-icon "chevron-down"}}</a>
|
||||
{{/if}}
|
||||
{{#if resultType.more}}
|
||||
<a href class="filter filter-type" {{action "moreOfType" resultType.type bubbles=false}}>{{i18n "show_more"}} {{fa-icon "chevron-down"}}</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
|
@ -314,23 +314,23 @@ const PostView = Discourse.GroupedView.extend(Ember.Evented, {
|
|||
}.on('willInsertElement'),
|
||||
|
||||
_applySearchHighlight: function() {
|
||||
const highlight = this.get('controller.searchHighlight');
|
||||
const highlight = this.get('searchService.highlightTerm');
|
||||
const cooked = this.$('.cooked');
|
||||
|
||||
if(!cooked){ return; }
|
||||
if (!cooked) { return; }
|
||||
|
||||
if(highlight && highlight.length > 2){
|
||||
if(this._highlighted){
|
||||
if (highlight && highlight.length > 2) {
|
||||
if (this._highlighted) {
|
||||
cooked.unhighlight();
|
||||
}
|
||||
cooked.highlight(highlight.split(/\s+/));
|
||||
this._highlighted = true;
|
||||
|
||||
} else if(this._highlighted){
|
||||
} else if (this._highlighted) {
|
||||
cooked.unhighlight();
|
||||
this._highlighted = false;
|
||||
}
|
||||
}.observes('controller.searchHighlight', 'cooked')
|
||||
}.observes('searchService.highlightTerm', 'cooked')
|
||||
});
|
||||
|
||||
export default PostView;
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
export default Ember.View.extend({
|
||||
tagName: 'div',
|
||||
classNames: ['d-dropdown'],
|
||||
elementId: 'search-dropdown',
|
||||
templateName: 'search',
|
||||
keyDown: function(e){
|
||||
var term = this.get('controller.term');
|
||||
if (e.which === 13 && term && term.length >= Discourse.SiteSettings.min_search_term_length) {
|
||||
this.get('controller').send('fullSearch');
|
||||
}
|
||||
}
|
||||
});
|
|
@ -104,3 +104,4 @@
|
|||
//= require_tree ./discourse/routes
|
||||
//= require_tree ./discourse/pre-initializers
|
||||
//= require_tree ./discourse/initializers
|
||||
//= require_tree ./discourse/services
|
||||
|
|
|
@ -154,27 +154,6 @@
|
|||
list-style: none;
|
||||
}
|
||||
|
||||
li:not(.category):not(.heading) {
|
||||
font-size: 0.929em;
|
||||
line-height: 16px;
|
||||
|
||||
.fa {
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
a {
|
||||
display: block;
|
||||
padding: 5px;
|
||||
transition: all linear .15s;
|
||||
}
|
||||
|
||||
&:hover a:not(.badge-notification) {
|
||||
background-color: dark-light-diff($highlight, $secondary, 50%, -55%);
|
||||
}
|
||||
|
||||
button {margin-left: 5px;}
|
||||
}
|
||||
|
||||
.heading a:hover {
|
||||
background-color: dark-light-diff($highlight, $secondary, 50%, -55%);
|
||||
}
|
||||
|
@ -229,47 +208,6 @@
|
|||
|
||||
// Search
|
||||
|
||||
&#search-dropdown {
|
||||
.heading {
|
||||
padding: 5px 0 5px 5px;
|
||||
.filter {
|
||||
padding: 0 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input[type='text'] {
|
||||
width: 518px;
|
||||
height: 22px;
|
||||
margin: 5px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.search-context {
|
||||
padding: 0 5px;
|
||||
label { margin-bottom: 0; }
|
||||
}
|
||||
|
||||
.searching {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
.spinner {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-width: 2px;
|
||||
margin: 20px 0 0 0;
|
||||
}
|
||||
}
|
||||
// I am ghetto using this to display "Show More".. be warned
|
||||
.no-results {
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
.filter {
|
||||
padding: 0;
|
||||
&:hover {background: transparent;}
|
||||
}
|
||||
|
||||
// Categories
|
||||
|
||||
|
@ -286,24 +224,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.search-link {
|
||||
.badge-category-parent {
|
||||
line-height: 0.8em;
|
||||
}
|
||||
.topic-title {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.topic-statuses {
|
||||
float: none;
|
||||
display: inline-block;
|
||||
color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%));
|
||||
margin: 0;
|
||||
.fa {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.highlight-strong {
|
||||
background-color: dark-light-diff($highlight, $secondary, 40%, -45%);
|
||||
|
@ -313,27 +233,6 @@
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
.search-context .show-help {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.search-context {
|
||||
min-height: 30px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.d-dropdown#search-dropdown {
|
||||
max-height: none;
|
||||
overflow: inherit;
|
||||
}
|
||||
|
||||
#search-dropdown .results {
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#search-help table td {
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
position: absolute;
|
||||
top: 40px;
|
||||
bottom: 37px;
|
||||
width: 96%;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -48,6 +49,7 @@
|
|||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.hamburger-panel {
|
||||
|
@ -83,3 +85,89 @@
|
|||
}
|
||||
}
|
||||
|
||||
.search-menu {
|
||||
|
||||
.search-context .show-help {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.heading {
|
||||
padding: 5px 0 5px 5px;
|
||||
.filter {
|
||||
padding: 0 5px;
|
||||
}
|
||||
}
|
||||
|
||||
input[type='text'] {
|
||||
width: 93%;
|
||||
height: 22px;
|
||||
margin: 5px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.search-context {
|
||||
padding: 0 5px;
|
||||
label { margin-bottom: 0; }
|
||||
}
|
||||
|
||||
.searching {
|
||||
position: absolute;
|
||||
top: 0.1em;
|
||||
right: 1.25em;
|
||||
.spinner {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-width: 2px;
|
||||
margin: 20px 0 0 0;
|
||||
}
|
||||
}
|
||||
// I am ghetto using this to display "Show More".. be warned
|
||||
.no-results {
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
.filter {
|
||||
padding: 0;
|
||||
&:hover {background: transparent;}
|
||||
}
|
||||
|
||||
.search-link {
|
||||
.badge-category-parent {
|
||||
line-height: 0.8em;
|
||||
}
|
||||
.topic-title {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.topic-statuses {
|
||||
float: none;
|
||||
display: inline-block;
|
||||
color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%));
|
||||
margin: 0;
|
||||
.fa {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
li:not(.category):not(.heading) {
|
||||
font-size: 0.929em;
|
||||
line-height: 16px;
|
||||
|
||||
.fa {
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
a {
|
||||
display: block;
|
||||
padding: 5px;
|
||||
transition: all linear .15s;
|
||||
}
|
||||
|
||||
&:hover a:not(.badge-notification) {
|
||||
background-color: dark-light-diff($highlight, $secondary, 50%, -55%);
|
||||
}
|
||||
|
||||
button {margin-left: 5px;}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,15 +23,8 @@ test("header", () => {
|
|||
// Search
|
||||
click("#search-button");
|
||||
andThen(() => {
|
||||
ok(exists("#search-dropdown:visible"), "after clicking a button search box opens");
|
||||
not(exists("#search-dropdown .heading"), "initially, immediately after opening, search box is empty");
|
||||
ok(exists(".search-menu:visible"), "after clicking a button search box opens");
|
||||
not(exists(".search-menu .heading"), "initially, immediately after opening, search box is empty");
|
||||
});
|
||||
|
||||
// Perform Search
|
||||
// TODO how do I fix the fixture to be a POST instead of a GET @eviltrout
|
||||
// fillIn("#search-term", "hello");
|
||||
// andThen(() => {
|
||||
// ok(exists("#search-dropdown .heading"), "when user completes a search, search box shows search results");
|
||||
// equal(find("#search-dropdown .results a:first").attr("href"), "/t/hello-bar-integration-issues/17638", "there is a search result");
|
||||
// });
|
||||
});
|
||||
|
|
|
@ -8,11 +8,11 @@ test("search", (assert) => {
|
|||
|
||||
andThen(() => {
|
||||
assert.ok(exists('#search-term'), 'it shows the search bar');
|
||||
assert.ok(!exists('#search-dropdown .results ul li'), 'no results by default');
|
||||
assert.ok(!exists('.search-menu .results ul li'), 'no results by default');
|
||||
});
|
||||
|
||||
fillIn('#search-term', 'dev');
|
||||
andThen(() => {
|
||||
assert.ok(exists('#search-dropdown .results ul li'), 'it shows results');
|
||||
assert.ok(exists('.search-menu .results ul li'), 'it shows results');
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue