From d4b987ff32f77e99998dbb234c3cb0dc0a568135 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 26 Aug 2015 16:55:01 -0400 Subject: [PATCH] Migrate search drop down to `menu-panel` component. --- .../discourse/components/menu-panel.js.es6 | 65 +++++-- .../discourse/components/search-menu.js.es6 | 174 ++++++++++++++++++ .../components/search-text-field.js.es6 | 8 +- .../discourse/controllers/application.js.es6 | 2 +- .../discourse/controllers/header.js.es6 | 8 + .../discourse/controllers/search.js.es6 | 170 ----------------- .../discourse/controllers/topic.js.es6 | 21 +-- .../initializers/keyboard-shortcuts.js.es6 | 1 + .../discourse/lib/keyboard-shortcuts.js.es6 | 25 +-- .../inject-discourse-objects.js.es6 | 4 + .../discourse/routes/application.js.es6 | 7 - .../routes/build-category-route.js.es6 | 4 +- .../routes/build-user-topic-list-route.js.es6 | 4 +- .../javascripts/discourse/routes/topic.js.es6 | 11 +- .../javascripts/discourse/routes/user.js.es6 | 6 +- .../discourse/services/search.js.es6 | 30 +++ .../templates/components/search-menu.hbs | 44 +++++ .../discourse/templates/header.hbs | 14 +- .../discourse/templates/search.hbs | 37 ---- .../javascripts/discourse/views/post.js.es6 | 12 +- .../javascripts/discourse/views/search.js.es6 | 12 -- app/assets/javascripts/main_include.js | 1 + .../stylesheets/common/base/header.scss | 101 ---------- .../stylesheets/common/base/menu-panel.scss | 88 +++++++++ .../acceptance/header-anonymous-test.js.es6 | 11 +- .../javascripts/acceptance/search-test.js.es6 | 4 +- 26 files changed, 442 insertions(+), 422 deletions(-) create mode 100644 app/assets/javascripts/discourse/components/search-menu.js.es6 delete mode 100644 app/assets/javascripts/discourse/controllers/search.js.es6 create mode 100644 app/assets/javascripts/discourse/services/search.js.es6 create mode 100644 app/assets/javascripts/discourse/templates/components/search-menu.hbs delete mode 100644 app/assets/javascripts/discourse/templates/search.hbs delete mode 100644 app/assets/javascripts/discourse/views/search.js.es6 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"}} + +
+ {{#if searchService.searchContext}} + + {{/if}} + {{i18n "show_help"}} +
+
+ {{#if loading}} +
{{loading-spinner}}
+ {{else}} +
+ {{#if noResults}} +
+ {{i18n "search.no_results"}} +
+ {{else}} + {{#each content.resultTypes as |resultType|}} + +
+ {{#if resultType.moreUrl}} + {{i18n "show_more"}} {{fa-icon "chevron-down"}} + {{/if}} + {{#if resultType.more}} + {{i18n "show_more"}} {{fa-icon "chevron-down"}} + {{/if}} +
+ {{/each}} + {{/if}} +
+ {{/if}} +{{/menu-panel}} diff --git a/app/assets/javascripts/discourse/templates/header.hbs b/app/assets/javascripts/discourse/templates/header.hbs index c58e214b07f..ddca4a96856 100644 --- a/app/assets/javascripts/discourse/templates/header.hbs +++ b/app/assets/javascripts/discourse/templates/header.hbs @@ -1,4 +1,5 @@ {{hamburger-menu hamburgerVisible=hamburgerVisible showKeyboardAction="showKeyboardShortcutsHelp"}} +{{search-menu searchVisible=searchVisible}}
@@ -28,17 +29,15 @@ {{/if}} {{/if}} -
  • +
  • {{#if loginRequired}} - {{else}} - + {{fa-icon "search" label="search.title"}} {{/if}} @@ -81,7 +80,6 @@ {{#if view.renderDropdowns}} {{plugin-outlet "header-before-dropdowns"}} - {{render "search"}} {{render "notifications" notifications}} {{render "user-dropdown"}} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/search.hbs b/app/assets/javascripts/discourse/templates/search.hbs deleted file mode 100644 index 19eb370028a..00000000000 --- a/app/assets/javascripts/discourse/templates/search.hbs +++ /dev/null @@ -1,37 +0,0 @@ -{{plugin-outlet "above-search"}} -{{search-text-field value=term searchContextEnabled=searchContextEnabled id="search-term"}} - -
    - {{#if searchContext}} - - {{/if}} - {{i18n "show_help"}} -
    -{{#if loading}} -
    {{loading-spinner}}
    -{{else}} -
    - {{#if noResults}} -
    - {{i18n "search.no_results"}} -
    - {{else}} - {{#each content.resultTypes as |resultType|}} -
      -
    • {{resultType.name}}
    • - {{component resultType.componentName results=resultType.results term=term}} -
    -
    - {{#if resultType.moreUrl}} - {{i18n "show_more"}} {{fa-icon "chevron-down"}} - {{/if}} - {{#if resultType.more}} - {{i18n "show_more"}} {{fa-icon "chevron-down"}} - {{/if}} -
    - {{/each}} - {{/if}} -
    -{{/if}} diff --git a/app/assets/javascripts/discourse/views/post.js.es6 b/app/assets/javascripts/discourse/views/post.js.es6 index fba794c6054..bc555c67832 100644 --- a/app/assets/javascripts/discourse/views/post.js.es6 +++ b/app/assets/javascripts/discourse/views/post.js.es6 @@ -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; diff --git a/app/assets/javascripts/discourse/views/search.js.es6 b/app/assets/javascripts/discourse/views/search.js.es6 deleted file mode 100644 index 861a29305f4..00000000000 --- a/app/assets/javascripts/discourse/views/search.js.es6 +++ /dev/null @@ -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'); - } - } -}); diff --git a/app/assets/javascripts/main_include.js b/app/assets/javascripts/main_include.js index ce051e83d5b..e48e17368f2 100644 --- a/app/assets/javascripts/main_include.js +++ b/app/assets/javascripts/main_include.js @@ -104,3 +104,4 @@ //= require_tree ./discourse/routes //= require_tree ./discourse/pre-initializers //= require_tree ./discourse/initializers +//= require_tree ./discourse/services diff --git a/app/assets/stylesheets/common/base/header.scss b/app/assets/stylesheets/common/base/header.scss index d7c33b2cd2a..e7664332768 100644 --- a/app/assets/stylesheets/common/base/header.scss +++ b/app/assets/stylesheets/common/base/header.scss @@ -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; } diff --git a/app/assets/stylesheets/common/base/menu-panel.scss b/app/assets/stylesheets/common/base/menu-panel.scss index 4a08b338c5c..a64a79a0b43 100644 --- a/app/assets/stylesheets/common/base/menu-panel.scss +++ b/app/assets/stylesheets/common/base/menu-panel.scss @@ -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;} + } +} diff --git a/test/javascripts/acceptance/header-anonymous-test.js.es6 b/test/javascripts/acceptance/header-anonymous-test.js.es6 index 6e642b72928..088cb4a30f5 100644 --- a/test/javascripts/acceptance/header-anonymous-test.js.es6 +++ b/test/javascripts/acceptance/header-anonymous-test.js.es6 @@ -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"); - // }); }); diff --git a/test/javascripts/acceptance/search-test.js.es6 b/test/javascripts/acceptance/search-test.js.es6 index 702cb1dde06..71208a74640 100644 --- a/test/javascripts/acceptance/search-test.js.es6 +++ b/test/javascripts/acceptance/search-test.js.es6 @@ -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'); }); });