diff --git a/app/assets/javascripts/discourse/components/menu-panel.js.es6 b/app/assets/javascripts/discourse/components/menu-panel.js.es6 index 8b88fb9bcaa..3333ffa2ab4 100644 --- a/app/assets/javascripts/discourse/components/menu-panel.js.es6 +++ b/app/assets/javascripts/discourse/components/menu-panel.js.es6 @@ -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') diff --git a/app/assets/javascripts/discourse/components/search-menu.js.es6 b/app/assets/javascripts/discourse/components/search-menu.js.es6 new file mode 100644 index 00000000000..e8e85b34920 --- /dev/null +++ b/app/assets/javascripts/discourse/components/search-menu.js.es6 @@ -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'); + } + } +}); 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 e833f661101..bf46ddf33f3 100644 --- a/app/assets/javascripts/discourse/components/search-text-field.js.es6 +++ b/app/assets/javascripts/discourse/components/search-text-field.js.es6 @@ -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'); + } }); diff --git a/app/assets/javascripts/discourse/controllers/application.js.es6 b/app/assets/javascripts/discourse/controllers/application.js.es6 index 20eb7f70ad0..93d2755d314 100644 --- a/app/assets/javascripts/discourse/controllers/application.js.es6 +++ b/app/assets/javascripts/discourse/controllers/application.js.es6 @@ -15,5 +15,5 @@ export default Ember.Controller.extend({ @computed loginRequired() { return Discourse.SiteSettings.login_required && !Discourse.User.current(); - } + }, }); diff --git a/app/assets/javascripts/discourse/controllers/header.js.es6 b/app/assets/javascripts/discourse/controllers/header.js.es6 index 352c867c5cc..f1749fc7c43 100644 --- a/app/assets/javascripts/discourse/controllers/header.js.es6 +++ b/app/assets/javascripts/discourse/controllers/header.js.es6 @@ -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'); } + } }); diff --git a/app/assets/javascripts/discourse/controllers/search.js.es6 b/app/assets/javascripts/discourse/controllers/search.js.es6 deleted file mode 100644 index 4c39f5dbcac..00000000000 --- a/app/assets/javascripts/discourse/controllers/search.js.es6 +++ /dev/null @@ -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); - } -}); diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6 index b9ef3a87bf5..2a6d0ffaf9b 100644 --- a/app/assets/javascripts/discourse/controllers/topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic.js.es6 @@ -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 diff --git a/app/assets/javascripts/discourse/initializers/keyboard-shortcuts.js.es6 b/app/assets/javascripts/discourse/initializers/keyboard-shortcuts.js.es6 index 41485db6e69..f4fbf10acc3 100644 --- a/app/assets/javascripts/discourse/initializers/keyboard-shortcuts.js.es6 +++ b/app/assets/javascripts/discourse/initializers/keyboard-shortcuts.js.es6 @@ -3,6 +3,7 @@ import KeyboardShortcuts from 'discourse/lib/keyboard-shortcuts'; export default { name: "keyboard-shortcuts", + initialize(container) { KeyboardShortcuts.bindEvents(Mousetrap, container); } diff --git a/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 index c3aee329c71..7485240c27a 100644 --- a/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 +++ b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 @@ -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); - } - }, + } }; diff --git a/app/assets/javascripts/discourse/pre-initializers/inject-discourse-objects.js.es6 b/app/assets/javascripts/discourse/pre-initializers/inject-discourse-objects.js.es6 index 5d78569d4fe..736f630f7cd 100644 --- a/app/assets/javascripts/discourse/pre-initializers/inject-discourse-objects.js.es6 +++ b/app/assets/javascripts/discourse/pre-initializers/inject-discourse-objects.js.es6 @@ -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'); diff --git a/app/assets/javascripts/discourse/routes/application.js.es6 b/app/assets/javascripts/discourse/routes/application.js.es6 index a8ede91294e..0b66dbdcd2a 100644 --- a/app/assets/javascripts/discourse/routes/application.js.es6 +++ b/app/assets/javascripts/discourse/routes/application.js.es6 @@ -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' }); diff --git a/app/assets/javascripts/discourse/routes/build-category-route.js.es6 b/app/assets/javascripts/discourse/routes/build-category-route.js.es6 index fe794b4482b..eaf26cc18c5 100644 --- a/app/assets/javascripts/discourse/routes/build-category-route.js.es6 +++ b/app/assets/javascripts/discourse/routes/build-category-route.js.es6 @@ -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: { diff --git a/app/assets/javascripts/discourse/routes/build-user-topic-list-route.js.es6 b/app/assets/javascripts/discourse/routes/build-user-topic-list-route.js.es6 index c14174fae71..5704cd96dbf 100644 --- a/app/assets/javascripts/discourse/routes/build-user-topic-list-route.js.es6 +++ b/app/assets/javascripts/discourse/routes/build-user-topic-list-route.js.es6 @@ -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'); } }); }; diff --git a/app/assets/javascripts/discourse/routes/topic.js.es6 b/app/assets/javascripts/discourse/routes/topic.js.es6 index 04d3ae202ce..5384dd3d211 100644 --- a/app/assets/javascripts/discourse/routes/topic.js.es6 +++ b/app/assets/javascripts/discourse/routes/topic.js.es6 @@ -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); diff --git a/app/assets/javascripts/discourse/routes/user.js.es6 b/app/assets/javascripts/discourse/routes/user.js.es6 index cfbbf173e0e..cf7ea55c0ff 100644 --- a/app/assets/javascripts/discourse/routes/user.js.es6 +++ b/app/assets/javascripts/discourse/routes/user.js.es6 @@ -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) } }); diff --git a/app/assets/javascripts/discourse/services/search.js.es6 b/app/assets/javascripts/discourse/services/search.js.es6 new file mode 100644 index 00000000000..dcaa600aa61 --- /dev/null +++ b/app/assets/javascripts/discourse/services/search.js.es6 @@ -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'); + } + }, + +}); diff --git a/app/assets/javascripts/discourse/templates/components/search-menu.hbs b/app/assets/javascripts/discourse/templates/components/search-menu.hbs new file mode 100644 index 00000000000..5fa83dc39e6 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/search-menu.hbs @@ -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"}} + +