From 514c3976f05785f487d275af1e96c43533e7a478 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 14 Apr 2016 15:23:05 -0400 Subject: [PATCH] PERF: Migrate header to discourse widgets --- .../components/hamburger-menu.js.es6 | 76 ----- .../components/header-dropdown.js.es6 | 7 - .../components/header-extra-info.js.es6 | 55 +--- .../discourse/components/home-logo.js.es6 | 58 ---- .../discourse/components/menu-panel.js.es6 | 224 -------------- .../discourse/components/mount-widget.js.es6 | 66 +++-- .../components/notification-item.js.es6 | 105 ------- .../components/scrolling-post-stream.js.es6 | 19 ++ .../discourse/components/search-menu.js.es6 | 162 ---------- .../components/search-result-category.js.es6 | 2 - .../components/search-result-post.js.es6 | 2 - .../components/search-result-topic.js.es6 | 2 - .../components/search-result-user.js.es6 | 2 - .../discourse/components/search-result.js.es6 | 11 - .../discourse/components/site-header.js.es6 | 187 ++++++++++++ .../discourse/components/user-menu.js.es6 | 104 ------- .../user-notifications-large.js.es6 | 16 + .../discourse/controllers/composer.js.es6 | 2 +- .../discourse/controllers/header.js.es6 | 79 +---- .../discourse/controllers/topic.js.es6 | 11 +- .../controllers/user-notifications.js.es6 | 4 +- .../apply-flagged-properties.js.es6 | 2 +- .../subscribe-user-notifications.js.es6 | 7 +- .../discourse/lib/intercept-click.js.es6 | 1 + .../discourse/lib/keyboard-shortcuts.js.es6 | 47 +-- .../discourse/lib/plugin-api.js.es6 | 14 +- .../discourse/routes/application.js.es6 | 17 ++ .../discourse/routes/discourse.js.es6 | 1 - .../javascripts/discourse/routes/topic.js.es6 | 12 +- .../discourse/templates/application.hbs | 9 +- .../templates/components/hamburger-menu.hbs | 94 ------ .../templates/components/header-dropdown.hbs | 9 - .../components/header-extra-info.hbs | 21 -- .../templates/components/menu-links.hbs | 7 - .../templates/components/menu-panel.hbs | 7 - .../templates/components/search-menu.hbs | 40 --- .../components/search-result-category.hbs | 7 - .../components/search-result-post.hbs | 14 - .../components/search-result-topic.hbs | 14 - .../components/search-result-user.hbs | 8 - .../templates/components/user-menu.hbs | 48 --- ...{similar_topics.hbs => similar-topics.hbs} | 2 +- .../discourse/templates/header.hbs | 66 ----- .../templates/user/notifications-index.hbs | 9 +- .../templates/user/notifications.hbs | 4 +- .../discourse/views/composer.js.es6 | 2 +- .../javascripts/discourse/views/header.js.es6 | 55 ---- .../javascripts/discourse/views/topic.js.es6 | 28 +- .../discourse/views/user-notifications.js.es6 | 6 - .../discourse/widgets/actions-summary.js.es6 | 1 + .../discourse/widgets/button.js.es6 | 2 + .../discourse/widgets/click-hook.js.es6 | 64 ---- .../widgets/hamburger-categories.js.es6 | 2 +- .../discourse/widgets/hamburger-menu.js.es6 | 158 ++++++++++ .../widgets/header-topic-info.js.es6 | 54 ++++ .../discourse/widgets/header.js.es6 | 278 ++++++++++++++++++ .../discourse/widgets/home-logo.js.es6 | 48 +++ .../discourse/widgets/hooks.js.es6 | 67 +++++ .../javascripts/discourse/widgets/link.js.es6 | 84 ++++++ .../discourse/widgets/menu-panel.js.es6 | 32 ++ .../widgets/notification-item.js.es6 | 109 +++++++ .../discourse/widgets/post-gutter.js.es6 | 1 + .../javascripts/discourse/widgets/post.js.es6 | 3 +- .../discourse/widgets/raw-html.js.es6 | 6 +- .../widgets/search-menu-controls.js.es6 | 60 ++++ .../widgets/search-menu-results.js.es6 | 100 +++++++ .../discourse/widgets/search-menu.js.es6 | 166 +++++++++++ .../discourse/widgets/topic-map.js.es6 | 1 + .../discourse/widgets/topic-status.js.es6 | 39 +++ .../discourse/widgets/user-menu.js.es6 | 93 ++++++ .../widgets/user-notifications-large.js.es6 | 25 ++ .../widgets/user-notifications.js.es6 | 81 +++++ .../discourse/widgets/widget.js.es6 | 31 +- app/assets/javascripts/main_include.js | 3 +- .../stylesheets/common/base/header.scss | 2 +- .../hamburger-menu-staff-test.js.es6 | 13 - .../acceptance/hamburger-menu-test.js.es6 | 21 -- .../acceptance/header-anonymous-test.js.es6 | 30 -- .../acceptance/header-test-staff.js.es6 | 14 - .../javascripts/acceptance/search-test.js.es6 | 1 + .../components/menu-panel-test.js.es6 | 65 ---- .../javascripts/controllers/topic-test.js.es6 | 2 +- .../javascripts/helpers/component-test.js.es6 | 17 +- test/javascripts/helpers/qunit-helpers.js.es6 | 4 +- .../widgets/actions-summary-test.js.es6 | 4 +- .../widgets/hamburger-menu-test.js.es6 | 173 +++++++++++ test/javascripts/widgets/header-test.js.es6 | 37 +++ .../home-logo-test.js.es6 | 73 +++-- .../widgets/post-gutter-test.js.es6 | 2 + .../javascripts/widgets/user-menu-test.js.es6 | 106 +++++++ test/javascripts/widgets/widget-test.js.es6 | 2 + 91 files changed, 2179 insertions(+), 1640 deletions(-) delete mode 100644 app/assets/javascripts/discourse/components/hamburger-menu.js.es6 delete mode 100644 app/assets/javascripts/discourse/components/home-logo.js.es6 delete mode 100644 app/assets/javascripts/discourse/components/menu-panel.js.es6 delete mode 100644 app/assets/javascripts/discourse/components/notification-item.js.es6 delete mode 100644 app/assets/javascripts/discourse/components/search-menu.js.es6 delete mode 100644 app/assets/javascripts/discourse/components/search-result-category.js.es6 delete mode 100644 app/assets/javascripts/discourse/components/search-result-post.js.es6 delete mode 100644 app/assets/javascripts/discourse/components/search-result-topic.js.es6 delete mode 100644 app/assets/javascripts/discourse/components/search-result-user.js.es6 delete mode 100644 app/assets/javascripts/discourse/components/search-result.js.es6 create mode 100644 app/assets/javascripts/discourse/components/site-header.js.es6 delete mode 100644 app/assets/javascripts/discourse/components/user-menu.js.es6 create mode 100644 app/assets/javascripts/discourse/components/user-notifications-large.js.es6 delete mode 100644 app/assets/javascripts/discourse/templates/components/hamburger-menu.hbs delete mode 100644 app/assets/javascripts/discourse/templates/components/header-dropdown.hbs delete mode 100644 app/assets/javascripts/discourse/templates/components/header-extra-info.hbs delete mode 100644 app/assets/javascripts/discourse/templates/components/menu-links.hbs delete mode 100644 app/assets/javascripts/discourse/templates/components/menu-panel.hbs delete mode 100644 app/assets/javascripts/discourse/templates/components/search-menu.hbs delete mode 100644 app/assets/javascripts/discourse/templates/components/search-result-category.hbs delete mode 100644 app/assets/javascripts/discourse/templates/components/search-result-post.hbs delete mode 100644 app/assets/javascripts/discourse/templates/components/search-result-topic.hbs delete mode 100644 app/assets/javascripts/discourse/templates/components/search-result-user.hbs delete mode 100644 app/assets/javascripts/discourse/templates/components/user-menu.hbs rename app/assets/javascripts/discourse/templates/composer/{similar_topics.hbs => similar-topics.hbs} (64%) delete mode 100644 app/assets/javascripts/discourse/templates/header.hbs delete mode 100644 app/assets/javascripts/discourse/views/header.js.es6 delete mode 100644 app/assets/javascripts/discourse/views/user-notifications.js.es6 delete mode 100644 app/assets/javascripts/discourse/widgets/click-hook.js.es6 create mode 100644 app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 create mode 100644 app/assets/javascripts/discourse/widgets/header-topic-info.js.es6 create mode 100644 app/assets/javascripts/discourse/widgets/header.js.es6 create mode 100644 app/assets/javascripts/discourse/widgets/home-logo.js.es6 create mode 100644 app/assets/javascripts/discourse/widgets/hooks.js.es6 create mode 100644 app/assets/javascripts/discourse/widgets/link.js.es6 create mode 100644 app/assets/javascripts/discourse/widgets/menu-panel.js.es6 create mode 100644 app/assets/javascripts/discourse/widgets/notification-item.js.es6 create mode 100644 app/assets/javascripts/discourse/widgets/search-menu-controls.js.es6 create mode 100644 app/assets/javascripts/discourse/widgets/search-menu-results.js.es6 create mode 100644 app/assets/javascripts/discourse/widgets/search-menu.js.es6 create mode 100644 app/assets/javascripts/discourse/widgets/topic-status.js.es6 create mode 100644 app/assets/javascripts/discourse/widgets/user-menu.js.es6 create mode 100644 app/assets/javascripts/discourse/widgets/user-notifications-large.js.es6 create mode 100644 app/assets/javascripts/discourse/widgets/user-notifications.js.es6 delete mode 100644 test/javascripts/acceptance/hamburger-menu-staff-test.js.es6 delete mode 100644 test/javascripts/acceptance/hamburger-menu-test.js.es6 delete mode 100644 test/javascripts/acceptance/header-anonymous-test.js.es6 delete mode 100644 test/javascripts/acceptance/header-test-staff.js.es6 delete mode 100644 test/javascripts/components/menu-panel-test.js.es6 create mode 100644 test/javascripts/widgets/hamburger-menu-test.js.es6 create mode 100644 test/javascripts/widgets/header-test.js.es6 rename test/javascripts/{components => widgets}/home-logo-test.js.es6 (53%) create mode 100644 test/javascripts/widgets/user-menu-test.js.es6 diff --git a/app/assets/javascripts/discourse/components/hamburger-menu.js.es6 b/app/assets/javascripts/discourse/components/hamburger-menu.js.es6 deleted file mode 100644 index e00b175819a..00000000000 --- a/app/assets/javascripts/discourse/components/hamburger-menu.js.es6 +++ /dev/null @@ -1,76 +0,0 @@ -import computed from 'ember-addons/ember-computed-decorators'; -import mobile from 'discourse/lib/mobile'; - -export default Ember.Component.extend({ - classNames: ['hamburger-panel'], - - @computed('currentUser.read_faq') - prioritizeFaq(readFaq) { - // If it's a custom FAQ never prioritize it - return Ember.isEmpty(this.siteSettings.faq_url) && !readFaq; - }, - - @computed() - showKeyboardShortcuts() { - return !this.site.mobileView && !this.capabilities.touch; - }, - - @computed() - showMobileToggle() { - return this.site.mobileView || (this.siteSettings.enable_mobile_theme && this.capabilities.touch); - }, - - @computed() - mobileViewLinkTextKey() { - return this.site.mobileView ? "desktop_view" : "mobile_view"; - }, - - @computed() - faqUrl() { - return this.siteSettings.faq_url ? this.siteSettings.faq_url : Discourse.getURL('/faq'); - }, - - _lookupCount(type) { - const state = this.get('topicTrackingState'); - return state ? state.lookupCount(type) : 0; - }, - - @computed('topicTrackingState.messageCount') - newCount() { - return this._lookupCount('new'); - }, - - @computed('topicTrackingState.messageCount') - unreadCount() { - return this._lookupCount('unread'); - }, - - @computed() - categories() { - const hideUncategorized = !this.siteSettings.allow_uncategorized_topics; - const showSubcatList = this.siteSettings.show_subcategory_list; - const isStaff = Discourse.User.currentProp('staff'); - - return Discourse.Category.list().reject((c) => { - if (showSubcatList && c.get('parent_category_id')) { return true; } - if (hideUncategorized && c.get('isUncategorizedCategory') && !isStaff) { return true; } - return false; - }); - }, - - @computed() - showUserDirectoryLink() { - if (!this.siteSettings.enable_user_directory) return false; - if (this.siteSettings.hide_user_profiles_from_public && !this.currentUser) return false; - return true; - }, - - actions: { - keyboardShortcuts() { - this.sendAction('showKeyboardAction'); - }, - toggleMobileView() { - mobile.toggleMobileView(); - } - } -}); diff --git a/app/assets/javascripts/discourse/components/header-dropdown.js.es6 b/app/assets/javascripts/discourse/components/header-dropdown.js.es6 index c7479848554..af00290db8f 100644 --- a/app/assets/javascripts/discourse/components/header-dropdown.js.es6 +++ b/app/assets/javascripts/discourse/components/header-dropdown.js.es6 @@ -1,14 +1,7 @@ -import computed from 'ember-addons/ember-computed-decorators'; - export default Ember.Component.extend({ tagName: 'li', classNameBindings: [':header-dropdown-toggle', 'active'], - @computed('showUser', 'path') - href(showUser, path) { - return showUser ? this.currentUser.get('path') : Discourse.getURL(path); - }, - active: Ember.computed.alias('toggleVisible'), actions: { diff --git a/app/assets/javascripts/discourse/components/header-extra-info.js.es6 b/app/assets/javascripts/discourse/components/header-extra-info.js.es6 index 2182f3ea347..66aaf82cb91 100644 --- a/app/assets/javascripts/discourse/components/header-extra-info.js.es6 +++ b/app/assets/javascripts/discourse/components/header-extra-info.js.es6 @@ -1,54 +1,3 @@ -import DiscourseURL from 'discourse/lib/url'; - -const TopicCategoryComponent = Ember.Component.extend({ - needsSecondRow: Ember.computed.gt('secondRowItems.length', 0), - secondRowItems: function() { return []; }.property(), - - pmPath: function() { - var currentUser = this.get('currentUser'); - return currentUser && currentUser.pmPath(this.get('topic')); - }.property('topic'), - - showPrivateMessageGlyph: function() { - return !this.get('topic.is_warning') && this.get('topic.isPrivateMessage'); - }.property('topic.is_warning', 'topic.isPrivateMessage'), - - actions: { - jumpToTopPost() { - const topic = this.get('topic'); - if (topic) { - DiscourseURL.routeTo(topic.get('firstPostUrl')); - } - } - } - -}); - -let id = 0; - -// Allow us (and plugins) to register themselves as needing a second -// row in the header. If there is at least one thing in the second row -// the style changes to accomodate it. -function needsSecondRowIf(prop, cb) { - const rowId = "_second_row_" + (id++), - methodHash = {}; - - methodHash[id] = function() { - const secondRowItems = this.get('secondRowItems'), - propVal = this.get(prop); - if (cb.call(this, propVal)) { - secondRowItems.addObject(rowId); - } else { - secondRowItems.removeObject(rowId); - } - }.observes(prop).on('init'); - - TopicCategoryComponent.reopen(methodHash); +export function needsSecondRowIf() { + Ember.warn("DEPRECATION: `needsSecondRowIf` is deprecated. Use widget hooks on `header-second-row`"); } - -needsSecondRowIf('topic.category', function(cat) { - return cat && (!cat.get('isUncategorizedCategory') || !this.siteSettings.suppress_uncategorized_badge); -}); - -export default TopicCategoryComponent; -export { needsSecondRowIf }; diff --git a/app/assets/javascripts/discourse/components/home-logo.js.es6 b/app/assets/javascripts/discourse/components/home-logo.js.es6 deleted file mode 100644 index 4798205e2ec..00000000000 --- a/app/assets/javascripts/discourse/components/home-logo.js.es6 +++ /dev/null @@ -1,58 +0,0 @@ -import DiscourseURL from 'discourse/lib/url'; -import { iconHTML } from 'discourse/helpers/fa-icon'; -import { observes } from 'ember-addons/ember-computed-decorators'; - -export default Ember.Component.extend({ - widget: 'home-logo', - showMobileLogo: null, - linkUrl: null, - classNames: ['title'], - - init() { - this._super(); - this.showMobileLogo = this.site.mobileView && !Ember.isEmpty(this.siteSettings.mobile_logo_url); - this.linkUrl = this.get('targetUrl') || '/'; - }, - - @observes('minimized') - _updateLogo() { - // On mobile we don't minimize the logo - if (!this.site.mobileView) { - this.rerender(); - } - }, - - click(e) { - // if they want to open in a new tab, let it so - if (e.shiftKey || e.metaKey || e.ctrlKey || e.which === 2) { return true; } - - e.preventDefault(); - - DiscourseURL.routeTo(this.linkUrl); - return false; - }, - - render(buffer) { - const { siteSettings } = this; - const logoUrl = siteSettings.logo_url || ''; - const title = siteSettings.title; - - buffer.push(``); - if (!this.site.mobileView && this.get('minimized')) { - const logoSmallUrl = siteSettings.logo_small_url || ''; - if (logoSmallUrl.length) { - buffer.push(``); - } else { - buffer.push(iconHTML('home')); - } - } else if (this.showMobileLogo) { - buffer.push(``); - } else if (logoUrl.length) { - buffer.push(``); - } else { - buffer.push(``); - } - buffer.push(''); - } - -}); diff --git a/app/assets/javascripts/discourse/components/menu-panel.js.es6 b/app/assets/javascripts/discourse/components/menu-panel.js.es6 deleted file mode 100644 index ca017a1171f..00000000000 --- a/app/assets/javascripts/discourse/components/menu-panel.js.es6 +++ /dev/null @@ -1,224 +0,0 @@ -import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators'; -import { headerHeight } from 'discourse/views/header'; - -const PANEL_BODY_MARGIN = 30; -const mutationSupport = !Ember.testing && !!window['MutationObserver']; - -export default Ember.Component.extend({ - classNameBindings: [':menu-panel', 'visible::hidden', 'viewMode'], - _lastVisible: false, - - showClose: Ember.computed.equal('viewMode', 'slide-in'), - - _layoutComponent() { - if (!this.get('visible')) { return; } - - const $window = $(window); - let width = this.get('maxWidth') || 300; - const windowWidth = parseInt($window.width()); - - if ((windowWidth - width) < 50) { - width = windowWidth - 50; - } - - const viewMode = this.get('viewMode'); - const $panelBody = this.$('.panel-body'); - let contentHeight = parseInt(this.$('.panel-body-contents').height()); - - // We use a mutationObserver to check for style changes, so it's important - // we don't set it if it doesn't change. Same goes for the $panelBody! - const style = this.$().prop('style'); - - if (viewMode === 'drop-down') { - const $buttonPanel = $('header ul.icons'); - if ($buttonPanel.length === 0) { return; } - - // These values need to be set here, not in the css file - this is to deal with the - // possibility of the window being resized and the menu changing from .slide-in to .drop-down. - if (style.top !== '100%' || style.height !== 'auto') { - this.$().css({ top: '100%', height: 'auto' }); - } - - // adjust panel height - const fullHeight = parseInt($window.height()); - const offsetTop = this.$().offset().top; - const scrollTop = $window.scrollTop(); - - if (contentHeight + (offsetTop - scrollTop) + PANEL_BODY_MARGIN > fullHeight) { - contentHeight = fullHeight - (offsetTop - scrollTop) - PANEL_BODY_MARGIN; - } - if ($panelBody.height() !== contentHeight) { - $panelBody.height(contentHeight); - } - $('body').addClass('drop-down-visible'); - } else { - const menuTop = headerHeight(); - - let height; - const winHeight = $(window).height() - 16; - if ((menuTop + contentHeight) < winHeight) { - height = contentHeight + "px"; - } else { - height = winHeight - menuTop; - } - - if ($panelBody.prop('style').height !== '100%') { - $panelBody.height('100%'); - } - if (style.top !== menuTop + "px" || style.height !== height) { - this.$().css({ top: menuTop + "px", height }); - } - $('body').removeClass('drop-down-visible'); - } - - this.$().width(width); - }, - - @computed('force') - viewMode() { - const force = this.get('force'); - if (force) { return force; } - - const headerWidth = $('#main-outlet .container').width() || 1100; - const screenWidth = $(window).width(); - const remaining = parseInt((screenWidth - headerWidth) / 2); - - return (remaining < 50) ? 'slide-in' : 'drop-down'; - }, - - @observes('viewMode', 'visible') - _visibleChanged() { - if (this.get('visible')) { - // Allow us to hook into things being shown - if (!this._lastVisible) { - Ember.run.scheduleOnce('afterRender', () => this.sendAction('onVisible')); - this._lastVisible = true; - } - - $('html').on('click.close-menu-panel', (e) => { - const $target = $(e.target); - if ($target.closest('.header-dropdown-toggle').length > 0) { return; } - if ($target.closest('.menu-panel').length > 0) { return; } - this.hide(); - }); - this.performLayout(); - this._watchSizeChanges(); - - // iOS does not handle scroll events well - if (!this.capabilities.isIOS) { - $(window).on('scroll.discourse-menu-panel', () => this.performLayout()); - } - } else if (this._lastVisible) { - this._lastVisible = false; - Ember.run.scheduleOnce('afterRender', () => this.sendAction('onHidden')); - $('html').off('click.close-menu-panel'); - $(window).off('scroll.discourse-menu-panel'); - this._stopWatchingSize(); - $('body').removeClass('drop-down-visible'); - } - }, - - @computed() - showKeyboardShortcuts() { - return !this.site.mobileView && !this.capabilities.touch; - }, - - @computed() - showMobileToggle() { - return this.site.mobileView || (this.siteSettings.enable_mobile_theme && this.capabilities.touch); - }, - - @computed() - mobileViewLinkTextKey() { - return this.site.mobileView ? "desktop_view" : "mobile_view"; - }, - - @computed() - faqUrl() { - 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, characterData: true, attributes: true }); - } else { - clearInterval(this._resizeInterval); - this._resizeInterval = setInterval(() => { - Ember.run(() => { - const $panelBodyContents = this.$('.panel-body-contents'); - if ($panelBodyContents && $panelBodyContents.length) { - const contentHeight = parseInt($panelBodyContents.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', e => { - if (e.metaKey || e.ctrlKey || e.shiftKey) { return; } - const $target = $(e.target); - if ($target.data('ember-action') || $target.closest('.search-link').length > 0) { return; } - this.hide(); - }); - - this.appEvents.on('dropdowns:closeAll', this, this.hide); - this.appEvents.on('dom:clean', this, this.hide); - - $('body').on('keydown.discourse-menu-panel', e => { - if (e.which === 27) { - this.hide(); - } - }); - - $(window).on('resize.discourse-menu-panel', () => { - this.propertyDidChange('viewMode'); - this.performLayout(); - }); - - if (mutationSupport) { - this._observer = new MutationObserver(() => { - Ember.run.debounce(this, this.performLayout, 50); - }); - } - - this.propertyDidChange('viewMode'); - }, - - @on('willDestroyElement') - _removeEvents() { - this.appEvents.off('dom:clean', this, this.hide); - this.appEvents.off('dropdowns:closeAll', this, this.hide); - this.$().off('click.discourse-menu-panel'); - $('body').off('keydown.discourse-menu-panel'); - $('html').off('click.close-menu-panel'); - $(window).off('resize.discourse-menu-panel'); - $(window).off('scroll.discourse-menu-panel'); - }, - - hide() { - this.set('visible', false); - }, - - actions: { - close() { - this.hide(); - } - } -}); diff --git a/app/assets/javascripts/discourse/components/mount-widget.js.es6 b/app/assets/javascripts/discourse/components/mount-widget.js.es6 index b7a8fdb0207..85d43d37c84 100644 --- a/app/assets/javascripts/discourse/components/mount-widget.js.es6 +++ b/app/assets/javascripts/discourse/components/mount-widget.js.es6 @@ -1,5 +1,6 @@ +import { keyDirty } from 'discourse/widgets/widget'; import { diff, patch } from 'virtual-dom'; -import { WidgetClickHook } from 'discourse/widgets/click-hook'; +import { WidgetClickHook } from 'discourse/widgets/hooks'; import { renderedKey, queryRegistry } from 'discourse/widgets/widget'; const _cleanCallbacks = {}; @@ -13,13 +14,20 @@ export default Ember.Component.extend({ _rootNode: null, _timeout: null, _widgetClass: null, - _afterRender: null, + _renderCallback: null, + _childEvents: null, init() { this._super(); const name = this.get('widget'); this._widgetClass = queryRegistry(name) || this.container.lookupFactory(`widget:${name}`); + + if (!this._widgetClass) { + console.error(`Error: Could not find widget: ${name}`); + } + + this._childEvents = []; this._connected = []; }, @@ -42,50 +50,64 @@ export default Ember.Component.extend({ }, willDestroyElement() { + this._childEvents.forEach(evt => this.appEvents.off(evt)); Ember.run.cancel(this._timeout); }, + afterRender() { + }, + + beforePatch() { + }, + + afterPatch() { + }, + + dispatch(eventName, key) { + this._childEvents.push(eventName); + this.appEvents.on(eventName, refreshArg => { + const onRefresh = Ember.String.camelize(eventName.replace(/:/, '-')); + keyDirty(key, { onRefresh, refreshArg }); + this.queueRerender(); + }); + }, + queueRerender(callback) { - if (callback && !this._afterRender) { - this._afterRender = callback; + if (callback && !this._renderCallback) { + this._renderCallback = callback; } Ember.run.scheduleOnce('render', this, this.rerenderWidget); }, + buildArgs() { + }, + rerenderWidget() { Ember.run.cancel(this._timeout); if (this._rootNode) { + if (!this._widgetClass) { return; } + const t0 = new Date().getTime(); + const args = this.get('args') || this.buildArgs(); const opts = { model: this.get('model') }; - const newTree = new this._widgetClass(this.get('args'), this.container, opts); + const newTree = new this._widgetClass(args, this.container, opts); newTree._emberView = this; const patches = diff(this._tree || this._rootNode, newTree); - const $body = $(document); - const prevHeight = $body.height(); - const prevScrollTop = $body.scrollTop(); - + this.beforePatch(); this._rootNode = patch(this._rootNode, patches); - - const height = $body.height(); - const scrollTop = $body.scrollTop(); - - // This hack is for when swapping out many cloaked views at once - // when using keyboard navigation. It could suddenly move the - // scroll - if (prevHeight === height && scrollTop !== prevScrollTop) { - $body.scrollTop(prevScrollTop); - } + this.afterPatch(); this._tree = newTree; - if (this._afterRender) { - this._afterRender(); - this._afterRender = null; + if (this._renderCallback) { + this._renderCallback(); + this._renderCallback = null; } + this.afterRender(); renderedKey('*'); if (this.profileWidget) { diff --git a/app/assets/javascripts/discourse/components/notification-item.js.es6 b/app/assets/javascripts/discourse/components/notification-item.js.es6 deleted file mode 100644 index 64411038dfc..00000000000 --- a/app/assets/javascripts/discourse/components/notification-item.js.es6 +++ /dev/null @@ -1,105 +0,0 @@ -const LIKED_TYPE = 5; -const INVITED_TYPE = 8; -const GROUP_SUMMARY_TYPE = 16; - -export default Ember.Component.extend({ - tagName: 'li', - classNameBindings: ['notification.read', 'notification.is_warning'], - - name: function() { - var notificationType = this.get("notification.notification_type"); - var lookup = this.site.get("notificationLookup"); - return lookup[notificationType]; - }.property("notification.notification_type"), - - scope: function() { - if (this.get("name") === "custom") { - return this.get("notification.data.message"); - } else { - return "notifications." + this.get("name"); - } - }.property("name"), - - url: function() { - const it = this.get('notification'); - const badgeId = it.get("data.badge_id"); - if (badgeId) { - var badgeSlug = it.get("data.badge_slug"); - - if (!badgeSlug) { - const badgeName = it.get("data.badge_name"); - badgeSlug = badgeName.replace(/[^A-Za-z0-9_]+/g, '-').toLowerCase(); - } - - var username = it.get('data.username'); - username = username ? "?username=" + username.toLowerCase() : ""; - return Discourse.getURL('/badges/' + badgeId + '/' + badgeSlug + username); - } - - const topicId = it.get('topic_id'); - if (topicId) { - return Discourse.Utilities.postUrl(it.get("slug"), topicId, it.get("post_number")); - } - - if (it.get('notification_type') === INVITED_TYPE) { - return Discourse.getURL('/users/' + it.get('data.display_username')); - } - - if (it.get('data.group_id')) { - return Discourse.getURL('/users/' + it.get('data.username') + '/messages/group/' + it.get('data.group_name')); - } - - }.property("notification.data.{badge_id,badge_name,display_username}", "model.slug", "model.topic_id", "model.post_number"), - - description: function() { - const badgeName = this.get("notification.data.badge_name"); - if (badgeName) { return Discourse.Utilities.escapeExpression(badgeName); } - - const title = this.get('notification.data.topic_title'); - return Ember.isEmpty(title) ? "" : Discourse.Utilities.escapeExpression(title); - }.property("notification.data.{badge_name,topic_title}"), - - _markRead: function(){ - this.$('a').click(() => { - this.set('notification.read', true); - Discourse.setTransientHeader("Discourse-Clear-Notifications", this.get('notification.id')); - if (document && document.cookie) { - document.cookie = `cn=${this.get('notification.id')}; expires=Fri, 31 Dec 9999 23:59:59 GMT`; - } - return true; - }); - }.on('didInsertElement'), - - render(buffer) { - const notification = this.get('notification'); - // since we are reusing views now sometimes this can be unset - if (!notification) { return; } - const description = this.get('description'); - const username = notification.get('data.display_username'); - var text; - if (notification.get('notification_type') === GROUP_SUMMARY_TYPE) { - const count = notification.get('data.inbox_count'); - const group_name = notification.get('data.group_name'); - text = I18n.t(this.get('scope'), {count, group_name}); - } else if (notification.get('notification_type') === LIKED_TYPE && notification.get("data.count") > 1) { - const count = notification.get('data.count') - 2; - const username2 = notification.get('data.username2'); - if (count===0) { - text = I18n.t('notifications.liked_2', {description, username, username2}); - } else { - text = I18n.t('notifications.liked_many', {description, username, username2, count}); - } - } - else { - text = I18n.t(this.get('scope'), {description, username}); - } - text = Discourse.Emoji.unescape(text); - - const url = this.get('url'); - if (url) { - buffer.push('' + text + ''); - } else { - buffer.push(text); - } - } -}); diff --git a/app/assets/javascripts/discourse/components/scrolling-post-stream.js.es6 b/app/assets/javascripts/discourse/components/scrolling-post-stream.js.es6 index 38ad5fea692..c5625a81093 100644 --- a/app/assets/javascripts/discourse/components/scrolling-post-stream.js.es6 +++ b/app/assets/javascripts/discourse/components/scrolling-post-stream.js.es6 @@ -37,6 +37,25 @@ export default MountWidget.extend({ 'searchService'); }).volatile(), + beforePatch() { + const $body = $(document); + this.prevHeight = $body.height(); + this.prevScrollTop = $body.scrollTop(); + }, + + afterPatch() { + const $body = $(document); + const height = $body.height(); + const scrollTop = $body.scrollTop(); + + // This hack is for when swapping out many cloaked views at once + // when using keyboard navigation. It could suddenly move the + // scroll + if (this.prevHeight === height && scrollTop !== this.prevScrollTop) { + $body.scrollTop(this.prevScrollTop); + } + }, + scrolled() { if (this.isDestroyed || this.isDestroying) { return; } if (isWorkaroundActive()) { return; } diff --git a/app/assets/javascripts/discourse/components/search-menu.js.es6 b/app/assets/javascripts/discourse/components/search-menu.js.es6 deleted file mode 100644 index be80d20a263..00000000000 --- a/app/assets/javascripts/discourse/components/search-menu.js.es6 +++ /dev/null @@ -1,162 +0,0 @@ -import {searchForTerm, searchContextDescription, isValidSearchTerm } from 'discourse/lib/search'; -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) { - return searchContextDescription(Em.get(ctx, 'type'), Em.get(ctx, 'user.username') || Em.get(ctx, 'category.name')); - }, - - @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'); - if (isValidSearchTerm(term)) { - 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().select(); - }, - - 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) { - if (e.which === 13 && isValidSearchTerm(this.get('searchService.term'))) { - this.set('visible', false); - this.send('fullSearch'); - } - } -}); diff --git a/app/assets/javascripts/discourse/components/search-result-category.js.es6 b/app/assets/javascripts/discourse/components/search-result-category.js.es6 deleted file mode 100644 index e23284ed164..00000000000 --- a/app/assets/javascripts/discourse/components/search-result-category.js.es6 +++ /dev/null @@ -1,2 +0,0 @@ -import SearchResult from 'discourse/components/search-result'; -export default SearchResult.extend(); diff --git a/app/assets/javascripts/discourse/components/search-result-post.js.es6 b/app/assets/javascripts/discourse/components/search-result-post.js.es6 deleted file mode 100644 index e23284ed164..00000000000 --- a/app/assets/javascripts/discourse/components/search-result-post.js.es6 +++ /dev/null @@ -1,2 +0,0 @@ -import SearchResult from 'discourse/components/search-result'; -export default SearchResult.extend(); diff --git a/app/assets/javascripts/discourse/components/search-result-topic.js.es6 b/app/assets/javascripts/discourse/components/search-result-topic.js.es6 deleted file mode 100644 index e23284ed164..00000000000 --- a/app/assets/javascripts/discourse/components/search-result-topic.js.es6 +++ /dev/null @@ -1,2 +0,0 @@ -import SearchResult from 'discourse/components/search-result'; -export default SearchResult.extend(); diff --git a/app/assets/javascripts/discourse/components/search-result-user.js.es6 b/app/assets/javascripts/discourse/components/search-result-user.js.es6 deleted file mode 100644 index e23284ed164..00000000000 --- a/app/assets/javascripts/discourse/components/search-result-user.js.es6 +++ /dev/null @@ -1,2 +0,0 @@ -import SearchResult from 'discourse/components/search-result'; -export default SearchResult.extend(); diff --git a/app/assets/javascripts/discourse/components/search-result.js.es6 b/app/assets/javascripts/discourse/components/search-result.js.es6 deleted file mode 100644 index dacf1f26965..00000000000 --- a/app/assets/javascripts/discourse/components/search-result.js.es6 +++ /dev/null @@ -1,11 +0,0 @@ -export default Ember.Component.extend({ - tagName: 'ul', - - _highlightOnInsert: function() { - const term = this.get('controller.term'); - if(!_.isEmpty(term)) { - this.$('.blurb').highlight(term.split(/\s+/), {className: 'search-highlight'}); - this.$('.topic-title').highlight(term.split(/\s+/), {className: 'search-highlight'} ); - } - }.on('didInsertElement') -}); diff --git a/app/assets/javascripts/discourse/components/site-header.js.es6 b/app/assets/javascripts/discourse/components/site-header.js.es6 new file mode 100644 index 00000000000..bfd6bb32f58 --- /dev/null +++ b/app/assets/javascripts/discourse/components/site-header.js.es6 @@ -0,0 +1,187 @@ +import MountWidget from 'discourse/components/mount-widget'; +import { observes } from 'ember-addons/ember-computed-decorators'; + +const _flagProperties = []; +function addFlagProperty(prop) { + _flagProperties.pushObject(prop); +} + +const PANEL_BODY_MARGIN = 30; + +const SiteHeaderComponent = MountWidget.extend({ + widget: 'header', + docAt: null, + dockedHeader: null, + _topic: null, + + // profileWidget: true, + // classNameBindings: ['editingTopic'], + + @observes('currentUser.unread_notifications', 'currentUser.unread_private_messages') + _notificationsChanged() { + this.queueRerender(); + }, + + examineDockHeader() { + const $body = $('body'); + + // Check the dock after the current run loop. While rendering, + // it's much slower to calculate `outlet.offset()` + Ember.run.next(() => { + if (this.docAt === null) { + const outlet = $('#main-outlet'); + if (!(outlet && outlet.length === 1)) return; + this.docAt = outlet.offset().top; + } + + const offset = window.pageYOffset || $('html').scrollTop(); + if (offset >= this.docAt) { + if (!this.dockedHeader) { + $body.addClass('docked'); + this.dockedHeader = true; + } + } else { + if (this.dockedHeader) { + $body.removeClass('docked'); + this.dockedHeader = false; + } + } + }); + }, + + setTopic(topic) { + this._topic = topic; + this.queueRerender(); + }, + + didInsertElement() { + this._super(); + $(window).bind('scroll.discourse-dock', () => this.examineDockHeader()); + $(document).bind('touchmove.discourse-dock', () => this.examineDockHeader()); + $(window).on('resize.discourse-menu-panel', () => this.afterRender()); + + this.appEvents.on('header:show-topic', topic => this.setTopic(topic)); + this.appEvents.on('header:hide-topic', () => this.setTopic(null)); + + this.dispatch('notifications:changed', 'user-notifications'); + this.dispatch('header:keyboard-trigger', 'header'); + this.examineDockHeader(); + }, + + willDestroyElement() { + this._super(); + $(window).unbind('scroll.discourse-dock'); + $(document).unbind('touchmove.discourse-dock'); + $('body').off('keydown.header'); + this.appEvents.off('notifications:changed'); + $(window).off('resize.discourse-menu-panel'); + + this.appEvents.off('header:show-topic'); + this.appEvents.off('header:hide-topic'); + }, + + buildArgs() { + return { + flagCount: _flagProperties.reduce((prev, cur) => prev + this.get(cur), 0), + topic: this._topic, + canSignUp: this.get('canSignUp') + }; + }, + + afterRender() { + const $menuPanels = $('.menu-panel'); + if ($menuPanels.length === 0) { return; } + + const $window = $(window); + const windowWidth = parseInt($window.width()); + + + const headerWidth = $('#main-outlet .container').width() || 1100; + const remaining = parseInt((windowWidth - headerWidth) / 2); + const viewMode = (remaining < 50) ? 'slide-in' : 'drop-down'; + + $menuPanels.each((idx, panel) => { + const $panel = $(panel); + let width = parseInt($panel.attr('data-max-width') || 300); + if ((windowWidth - width) < 50) { + width = windowWidth - 50; + } + + $panel.removeClass('drop-down').removeClass('slide-in').addClass(viewMode); + + const $panelBody = $('.panel-body', $panel); + let contentHeight = parseInt($('.panel-body-contents', $panel).height()); + + // We use a mutationObserver to check for style changes, so it's important + // we don't set it if it doesn't change. Same goes for the $panelBody! + const style = $panel.prop('style'); + + if (viewMode === 'drop-down') { + const $buttonPanel = $('header ul.icons'); + if ($buttonPanel.length === 0) { return; } + + // These values need to be set here, not in the css file - this is to deal with the + // possibility of the window being resized and the menu changing from .slide-in to .drop-down. + if (style.top !== '100%' || style.height !== 'auto') { + $panel.css({ top: '100%', height: 'auto' }); + } + + // adjust panel height + const fullHeight = parseInt($window.height()); + const offsetTop = $panel.offset().top; + const scrollTop = $window.scrollTop(); + + if (contentHeight + (offsetTop - scrollTop) + PANEL_BODY_MARGIN > fullHeight) { + contentHeight = fullHeight - (offsetTop - scrollTop) - PANEL_BODY_MARGIN; + } + if ($panelBody.height() !== contentHeight) { + $panelBody.height(contentHeight); + } + $('body').addClass('drop-down-visible'); + } else { + const menuTop = headerHeight(); + + let height; + const winHeight = $(window).height() - 16; + if ((menuTop + contentHeight) < winHeight) { + height = contentHeight + "px"; + } else { + height = winHeight - menuTop; + } + + if ($panelBody.prop('style').height !== '100%') { + $panelBody.height('100%'); + } + if (style.top !== menuTop + "px" || style.height !== height) { + $panel.css({ top: menuTop + "px", height }); + } + $('body').removeClass('drop-down-visible'); + } + + $panel.width(width); + }); + } +}); + +export default SiteHeaderComponent; + +function applyFlaggedProperties() { + const args = _flagProperties.slice(); + args.push(function() { + this.queueRerender(); + }.on('init')); + + SiteHeaderComponent.reopen({ _flagsChanged: Ember.observer.apply(this, args) }); +} + +addFlagProperty('currentUser.site_flagged_posts_count'); +addFlagProperty('currentUser.post_queue_new_count'); + +export { addFlagProperty, applyFlaggedProperties }; + +export function headerHeight() { + const $header = $('header.d-header'); + const headerOffset = $header.offset(); + const headerOffsetTop = (headerOffset) ? headerOffset.top : 0; + return parseInt($header.outerHeight() + headerOffsetTop - $(window).scrollTop()); +} diff --git a/app/assets/javascripts/discourse/components/user-menu.js.es6 b/app/assets/javascripts/discourse/components/user-menu.js.es6 deleted file mode 100644 index d864f23ba49..00000000000 --- a/app/assets/javascripts/discourse/components/user-menu.js.es6 +++ /dev/null @@ -1,104 +0,0 @@ -import { url } from 'discourse/lib/computed'; -import { default as computed, observes } from 'ember-addons/ember-computed-decorators'; -import { headerHeight } from 'discourse/views/header'; - -export default Ember.Component.extend({ - classNames: ['user-menu'], - notifications: null, - loadingNotifications: false, - notificationsPath: url('currentUser.path', '%@/notifications'), - bookmarksPath: url('currentUser.path', '%@/activity/bookmarks'), - messagesPath: url('currentUser.path', '%@/messages'), - preferencesPath: url('currentUser.path', '%@/preferences'), - - @computed('allowAnon', 'isAnon') - showEnableAnon(allowAnon, isAnon) { return allowAnon && !isAnon; }, - - @computed('allowAnon', 'isAnon') - showDisableAnon(allowAnon, isAnon) { return allowAnon && isAnon; }, - - @observes('visible') - _loadNotifications() { - if (this.get("visible")) { - this.refreshNotifications(); - } - }, - - @observes('currentUser.lastNotificationChange') - _resetCachedNotifications() { - const visible = this.get('visible'); - - if (!Discourse.get("hasFocus")) { - this.set('visible', false); - this.set('notifications', null); - return; - } - - if (visible) { - this.refreshNotifications(); - } else { - this.set('notifications', null); - } - }, - - refreshNotifications() { - if (this.get('loadingNotifications')) { return; } - - // estimate (poorly) the amount of notifications to return - var limit = Math.round(($(window).height() - headerHeight()) / 55); - // we REALLY don't want to be asking for negative counts of notifications - // less than 5 is also not that useful - if (limit < 5) { limit = 5; } - if (limit > 40) { limit = 40; } - - // TODO: It's a bit odd to use the store in a component, but this one really - // wants to reach out and grab notifications - const store = this.container.lookup('store:main'); - const stale = store.findStale('notification', {recent: true, limit }, {cacheKey: 'recent-notifications'}); - - if (stale.hasResults) { - const results = stale.results; - var content = results.get('content'); - - // we have to truncate to limit, otherwise we will render too much - if (content && (content.length > limit)) { - content = content.splice(0, limit); - results.set('content', content); - results.set('totalRows', limit); - } - - this.set('notifications', results); - } else { - this.set('loadingNotifications', true); - } - - stale.refresh().then((notifications) => { - this.set('currentUser.unread_notifications', 0); - this.set('notifications', notifications); - }).catch(() => { - this.set('notifications', null); - }).finally(() => { - this.set('loadingNotifications', false); - }); - }, - - @computed() - allowAnon() { - return this.siteSettings.allow_anonymous_posting && - (this.get("currentUser.trust_level") >= this.siteSettings.anonymous_posting_min_trust_level || - this.get("isAnon")); - }, - - isAnon: Ember.computed.alias('currentUser.is_anonymous'), - - actions: { - toggleAnon() { - Discourse.ajax("/users/toggle-anon", {method: 'POST'}).then(function(){ - window.location.reload(); - }); - }, - logout() { - this.sendAction('logoutAction'); - } - } -}); diff --git a/app/assets/javascripts/discourse/components/user-notifications-large.js.es6 b/app/assets/javascripts/discourse/components/user-notifications-large.js.es6 new file mode 100644 index 00000000000..bfedea00f7f --- /dev/null +++ b/app/assets/javascripts/discourse/components/user-notifications-large.js.es6 @@ -0,0 +1,16 @@ +import MountWidget from 'discourse/components/mount-widget'; +import { observes } from "ember-addons/ember-computed-decorators"; + +export default MountWidget.extend({ + widget: 'user-notifications-large', + + init() { + this._super(); + this.args = { notifications: this.get('notifications') }; + }, + + @observes('notifications.length') + _triggerRefresh() { + this.queueRerender(); + } +}); diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index 7333f6dd0a9..b94ca33476e 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -386,7 +386,7 @@ export default Ember.Controller.extend({ let message = this.get('similarTopicsMessage'); if (!message) { message = Discourse.ComposerMessage.create({ - templateName: 'composer/similar_topics', + templateName: 'composer/similar-topics', extraClass: 'similar-topics' }); this.set('similarTopicsMessage', message); diff --git a/app/assets/javascripts/discourse/controllers/header.js.es6 b/app/assets/javascripts/discourse/controllers/header.js.es6 index 57401135df6..2665f2afbd1 100644 --- a/app/assets/javascripts/discourse/controllers/header.js.es6 +++ b/app/assets/javascripts/discourse/controllers/header.js.es6 @@ -1,77 +1,6 @@ -import DiscourseURL from 'discourse/lib/url'; +import { addFlagProperty as realAddFlagProperty } from 'discourse/components/site-header'; -const HeaderController = Ember.Controller.extend({ - topic: null, - showExtraInfo: null, - hamburgerVisible: false, - searchVisible: false, - userMenuVisible: false, - needs: ['application'], - - canSignUp: Em.computed.alias('controllers.application.canSignUp'), - - showSignUpButton: function() { - return this.get('canSignUp') && !this.get('showExtraInfo'); - }.property('canSignUp', 'showExtraInfo'), - - showStarButton: function() { - return Discourse.User.current() && !this.get('topic.isPrivateMessage'); - }.property('topic.isPrivateMessage'), - - - actions: { - toggleSearch() { - this.toggleProperty('searchVisible'); - }, - showUserMenu() { - if (!this.get('userMenuVisible')) { - this.appEvents.trigger('dropdowns:closeAll'); - this.set('userMenuVisible', true); - } - }, - - fullPageSearch() { - const searchService = this.container.lookup('search-service:main'); - const context = searchService.get('searchContext'); - var params = ""; - - if (context) { - params = `?context=${context.type}&context_id=${context.id}&skip_context=true`; - } - - DiscourseURL.routeTo('/search' + params); - }, - toggleMenuPanel(visibleProp) { - this.toggleProperty(visibleProp); - this.appEvents.trigger('dropdowns:closeAll'); - }, - - toggleStar() { - const topic = this.get('topic'); - if (topic) topic.toggleStar(); - return false; - } - } -}); - -// Allow plugins to add to the sum of "flags" above the site map -const _flagProperties = []; -function addFlagProperty(prop) { - _flagProperties.pushObject(prop); +export function addFlagProperty(prop) { + Ember.warn("importing `addFlagProperty` is deprecated. Use the PluginAPI instead"); + realAddFlagProperty(prop); } - -function applyFlaggedProperties() { - const args = _flagProperties.slice(); - args.push(function() { - let sum = 0; - _flagProperties.forEach((fp) => sum += (this.get(fp) || 0)); - return sum; - }); - HeaderController.reopen({ flaggedPostsCount: Ember.computed.apply(this, args) }); -} - -addFlagProperty('currentUser.site_flagged_posts_count'); -addFlagProperty('currentUser.post_queue_new_count'); - -export { addFlagProperty, applyFlaggedProperties }; -export default HeaderController; diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6 index f70f8877bcf..da342c73a17 100644 --- a/app/assets/javascripts/discourse/controllers/topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic.js.es6 @@ -9,7 +9,7 @@ import Composer from 'discourse/models/composer'; import DiscourseURL from 'discourse/lib/url'; export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { - needs: ['header', 'modal', 'composer', 'quote-button', 'topic-progress', 'application'], + needs: ['modal', 'composer', 'quote-button', 'topic-progress', 'application'], multiSelect: false, allPostsSelected: false, editingTopic: false, @@ -472,11 +472,6 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { this.get('content').toggleStatus('archived'); }, - // Toggle the star on the topic - toggleStar() { - this.get('content').toggleStar(); - }, - clearPin() { this.get('content').clearPin(); }, @@ -625,10 +620,6 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { return false; }, - showStarButton: function() { - return Discourse.User.current() && !this.get('model.isPrivateMessage'); - }.property('model.isPrivateMessage'), - loadingHTML: function() { return spinnerHTML; }.property(), diff --git a/app/assets/javascripts/discourse/controllers/user-notifications.js.es6 b/app/assets/javascripts/discourse/controllers/user-notifications.js.es6 index c06137a9e1b..e39d9a67c57 100644 --- a/app/assets/javascripts/discourse/controllers/user-notifications.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user-notifications.js.es6 @@ -10,13 +10,13 @@ export default Ember.ArrayController.extend({ currentPath: Em.computed.alias('controllers.application.currentPath'), actions: { - resetNew: function() { + resetNew() { Discourse.ajax('/notifications/mark-read', { method: 'PUT' }).then(() => { this.setEach('read', true); }); }, - loadMore: function() { + loadMore() { this.get('model').loadMore(); } } diff --git a/app/assets/javascripts/discourse/initializers/apply-flagged-properties.js.es6 b/app/assets/javascripts/discourse/initializers/apply-flagged-properties.js.es6 index 54085954518..197b6459c69 100644 --- a/app/assets/javascripts/discourse/initializers/apply-flagged-properties.js.es6 +++ b/app/assets/javascripts/discourse/initializers/apply-flagged-properties.js.es6 @@ -1,4 +1,4 @@ -import { applyFlaggedProperties } from 'discourse/controllers/header'; +import { applyFlaggedProperties } from 'discourse/components/site-header'; export default { name: 'apply-flagged-properties', diff --git a/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6 b/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6 index 63ef5104b4d..406b050d3b8 100644 --- a/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6 +++ b/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6 @@ -10,7 +10,8 @@ export default { siteSettings = container.lookup('site-settings:main'), bus = container.lookup('message-bus:main'), keyValueStore = container.lookup('key-value-store:main'), - store = container.lookup('store:main'); + store = container.lookup('store:main'), + appEvents = container.lookup('app-events:main'); // clear old cached notifications, we used to store in local storage // TODO 2017 delete this line @@ -30,7 +31,7 @@ export default { }); } - bus.subscribe("/notification/" + user.get('id'), function(data) { + bus.subscribe(`/notification/${user.get('id')}`, function(data) { const oldUnread = user.get('unread_notifications'); const oldPM = user.get('unread_private_messages'); @@ -38,7 +39,7 @@ export default { user.set('unread_private_messages', data.unread_private_messages); if (oldUnread !== data.unread_notifications || oldPM !== data.unread_private_messages) { - user.set('lastNotificationChange', new Date()); + appEvents.trigger('notifications:changed'); } const stale = store.findStale('notification', {}, {cacheKey: 'recent-notifications'}); diff --git a/app/assets/javascripts/discourse/lib/intercept-click.js.es6 b/app/assets/javascripts/discourse/lib/intercept-click.js.es6 index 6ce7a300f37..658f55db64b 100644 --- a/app/assets/javascripts/discourse/lib/intercept-click.js.es6 +++ b/app/assets/javascripts/discourse/lib/intercept-click.js.es6 @@ -18,6 +18,7 @@ export default function interceptClick(e) { $currentTarget.data('auto-route') || $currentTarget.data('share-url') || $currentTarget.data('user-card') || + $currentTarget.hasClass('widget-link') || $currentTarget.hasClass('mention') || (!$currentTarget.hasClass('d-link') && $currentTarget.hasClass('ember-view')) || $currentTarget.hasClass('lightbox') || diff --git a/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 index 748099a740f..63aa464d1ed 100644 --- a/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 +++ b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 @@ -10,8 +10,8 @@ const bindings = { '.': {click: '.alert.alert-info.clickable', anonymous: true}, // show incoming/updated topics 'b': {handler: 'toggleBookmark'}, 'c': {handler: 'createTopic'}, - 'ctrl+f': {handler: 'showBuiltinSearch', anonymous: true}, - 'command+f': {handler: 'showBuiltinSearch', anonymous: true}, + 'ctrl+f': {handler: 'showPageSearch', anonymous: true}, + 'command+f': {handler: 'showPageSearch', anonymous: true}, 'd': {postAction: 'deletePost'}, 'e': {postAction: 'editPost'}, 'end': {handler: 'goToLastPost', anonymous: true}, @@ -142,32 +142,10 @@ export default { this._changeSection(-1); }, - showBuiltinSearch() { - if (this.container.lookup('controller:header').get('searchVisible')) { - this.toggleSearch(); - return true; - } - - this.searchService.set('searchContextEnabled', false); - - const currentPath = this.container.lookup('controller:application').get('currentPath'), - blacklist = [ /^discovery\.categories/ ], - whitelist = [ /^topic\./ ], - check = function(regex) { return !!currentPath.match(regex); }; - let showSearch = whitelist.any(check) && !blacklist.any(check); - - // If we're viewing a topic, only intercept search if there are cloaked posts - if (showSearch && currentPath.match(/^topic\./)) { - showSearch = $('.topic-post .cooked, .small-action:not(.time-gap)').length < this.container.lookup('controller:topic').get('model.postStream.stream.length'); - } - - if (showSearch) { - this.searchService.set('searchContextEnabled', true); - this.toggleSearch(); - return false; - } - - return true; + showPageSearch(event) { + Ember.run(() => { + this.appEvents.trigger('header:keyboard-trigger', {type: 'page-search', event}); + }); }, createTopic() { @@ -182,17 +160,16 @@ export default { this.container.lookup('controller:topic-progress').send('toggleExpansion', {highlight: true}); }, - toggleSearch() { - this.container.lookup('controller:header').send('toggleSearch'); - return false; + toggleSearch(event) { + this.appEvents.trigger('header:keyboard-trigger', {type: 'search', event}); }, - toggleHamburgerMenu() { - this.container.lookup('controller:header').send('toggleMenuPanel', 'hamburgerVisible'); + toggleHamburgerMenu(event) { + this.appEvents.trigger('header:keyboard-trigger', {type: 'hamburger', event}); }, - showCurrentUser() { - this.container.lookup('controller:header').send('toggleMenuPanel', 'userMenuVisible'); + showCurrentUser(event) { + this.appEvents.trigger('header:keyboard-trigger', {type: 'user', event}); }, showHelpModal() { diff --git a/app/assets/javascripts/discourse/lib/plugin-api.js.es6 b/app/assets/javascripts/discourse/lib/plugin-api.js.es6 index 178f48e5a9a..f1d31936f87 100644 --- a/app/assets/javascripts/discourse/lib/plugin-api.js.es6 +++ b/app/assets/javascripts/discourse/lib/plugin-api.js.es6 @@ -9,6 +9,7 @@ import { createWidget, decorateWidget, changeSetting } from 'discourse/widgets/w import { onPageChange } from 'discourse/lib/page-tracker'; import { preventCloak } from 'discourse/widgets/post-stream'; import { h } from 'virtual-dom'; +import { addFlagProperty } from 'discourse/components/site-header'; class PluginApi { constructor(version, container) { @@ -284,11 +285,20 @@ class PluginApi { createWidget(name, args) { return createWidget(name, args); } + + /** + * Adds a property that can be summed for calculating the flag counter + **/ + addFlagProperty(property) { + return addFlagProperty(property); + } + } let _pluginv01; function getPluginApi(version) { - if (version === "0.1" || version === "0.2" || version === "0.3") { + version = parseFloat(version); + if (version <= 0.4) { if (!_pluginv01) { _pluginv01 = new PluginApi(version, Discourse.__container__); } @@ -299,7 +309,7 @@ function getPluginApi(version) { } /** - * withPluginApi(version, apiCode, noApi) + * withPluginApi(version, apiCodeCallback, opts) * * Helper to version our client side plugin API. Pass the version of the API that your * plugin is coded against. If that API is available, the `apiCodeCallback` function will diff --git a/app/assets/javascripts/discourse/routes/application.js.es6 b/app/assets/javascripts/discourse/routes/application.js.es6 index b057bcba8c4..7954ade89f4 100644 --- a/app/assets/javascripts/discourse/routes/application.js.es6 +++ b/app/assets/javascripts/discourse/routes/application.js.es6 @@ -3,6 +3,7 @@ import logout from 'discourse/lib/logout'; import showModal from 'discourse/lib/show-modal'; import OpenComposer from "discourse/mixins/open-composer"; import Category from 'discourse/models/category'; +import mobile from 'discourse/lib/mobile'; function unlessReadOnly(method, message) { return function() { @@ -25,6 +26,22 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, { actions: { + showSearchHelp() { + Discourse.ajax("/static/search_help.html", { dataType: 'html' }).then(model => { + showModal('searchHelp', { model }); + }); + }, + + toggleAnonymous() { + Discourse.ajax("/users/toggle-anon", {method: 'POST'}).then(() => { + window.location.reload(); + }); + }, + + toggleMobileView() { + mobile.toggleMobileView(); + }, + logout: unlessReadOnly('_handleLogout', I18n.t("read_only_mode.logout_disabled")), _collectTitleTokens(tokens) { diff --git a/app/assets/javascripts/discourse/routes/discourse.js.es6 b/app/assets/javascripts/discourse/routes/discourse.js.es6 index 5576fb422ca..47fd25ed7db 100644 --- a/app/assets/javascripts/discourse/routes/discourse.js.es6 +++ b/app/assets/javascripts/discourse/routes/discourse.js.es6 @@ -75,7 +75,6 @@ const DiscourseRoute = Ember.Route.extend({ }); export function cleanDOM() { - if (window.MiniProfiler) { window.MiniProfiler.pageTransition(); } diff --git a/app/assets/javascripts/discourse/routes/topic.js.es6 b/app/assets/javascripts/discourse/routes/topic.js.es6 index 4297186f07f..cd627ce46d9 100644 --- a/app/assets/javascripts/discourse/routes/topic.js.es6 +++ b/app/assets/javascripts/discourse/routes/topic.js.es6 @@ -179,8 +179,9 @@ const TopicRoute = Discourse.Route.extend({ this.searchService.set('searchContext', null); this.controllerFor('user-card').set('visible', false); - const topicController = this.controllerFor('topic'), - postStream = topicController.get('model.postStream'); + const topicController = this.controllerFor('topic'); + const postStream = topicController.get('model.postStream'); + postStream.cancelFilter(); topicController.set('multiSelect', false); @@ -188,11 +189,7 @@ const TopicRoute = Discourse.Route.extend({ this.controllerFor('composer').set('topic', null); this.screenTrack.stop(); - const headerController = this.controllerFor('header'); - if (headerController) { - headerController.set('topic', null); - headerController.set('showExtraInfo', false); - } + this.appEvents.trigger('header:hide-topic'); }, setupController(controller, model) { @@ -207,7 +204,6 @@ const TopicRoute = Discourse.Route.extend({ TopicRoute.trigger('setupTopicController', this); - this.controllerFor('header').setProperties({ topic: model, showExtraInfo: false }); this.searchService.set('searchContext', model.get('searchContext')); this.controllerFor('composer').set('topic', model); diff --git a/app/assets/javascripts/discourse/templates/application.hbs b/app/assets/javascripts/discourse/templates/application.hbs index 99ef6ee552c..479d0c29259 100644 --- a/app/assets/javascripts/discourse/templates/application.hbs +++ b/app/assets/javascripts/discourse/templates/application.hbs @@ -1,4 +1,11 @@ -{{render "header"}} +{{site-header canSignUp=canSignUp + showCreateAccount="showCreateAccount" + showLogin="showLogin" + showKeyboard="showKeyboardShortcutsHelp" + toggleMobileView="toggleMobileView" + toggleAnonymous="toggleAnonymous" + logout="logout" + showSearchHelp="showSearchHelp"}}
diff --git a/app/assets/javascripts/discourse/templates/components/hamburger-menu.hbs b/app/assets/javascripts/discourse/templates/components/hamburger-menu.hbs deleted file mode 100644 index f0bc9397ab0..00000000000 --- a/app/assets/javascripts/discourse/templates/components/hamburger-menu.hbs +++ /dev/null @@ -1,94 +0,0 @@ -{{#menu-panel visible=visible}} - {{#if prioritizeFaq}} - {{#menu-links}} -
  • - {{#d-link path=faqUrl class="faq-link"}} - {{i18n "faq"}} - {{i18n "new_item"}} - {{/d-link}} -
  • - {{/menu-links}} - {{/if}} - - {{#if currentUser.staff}} - {{#menu-links}} -
  • {{d-link route="admin" class="admin-link" icon="wrench" label="admin_title"}}
  • -
  • - {{#d-link route="adminFlags" class="flagged-posts-link"}} - {{fa-icon "flag"}} {{i18n 'flags_title'}} - {{#if currentUser.site_flagged_posts_count}} - {{currentUser.site_flagged_posts_count}} - {{/if}} - {{/d-link}} -
  • - - {{#if currentUser.show_queued_posts}} -
  • - {{#d-link route='queued-posts'}} - {{i18n "queue.title"}} - {{#if currentUser.post_queue_new_count}} - {{currentUser.post_queue_new_count}} - {{/if}} - {{/d-link}} -
  • - {{/if}} - {{#if currentUser.admin}} -
  • {{d-link route="adminSiteSettings" icon="gear" label="admin.site_settings.title"}}
  • - {{/if}} - - {{plugin-outlet "hamburger-admin"}} - {{/menu-links}} - {{/if}} - - {{#menu-links}} -
  • {{d-link route="discovery.latest" class="latest-topics-link" label="filters.latest.title"}}
  • - - {{#if currentUser}} -
  • - {{#if newCount}} - {{d-link route="discovery.new" class="new-topics-link" label="filters.new.title_with_count" count=newCount}} - {{else}} - {{d-link route="discovery.new" class="new-topics-link" label="filters.new.title"}} - {{/if}} -
  • -
  • - {{#if unreadCount}} - {{d-link route="discovery.unread" class="unread-topics-link" label="filters.unread.title_with_count" count=unreadCount}} - {{else}} - {{d-link route="discovery.unread" class="unread-topics-link" label="filters.unread.title"}} - {{/if}} -
  • - {{/if}} -
  • {{d-link route="discovery.top" class="top-topics-link" label="filters.top.title"}}
  • - - {{#if siteSettings.enable_badges}} -
  • {{d-link route="badges" class="badge-link" label="badges.title"}}
  • - {{/if}} - - {{#if showUserDirectoryLink}} -
  • {{d-link route="users" class="user-directory-link" label="directory.title"}}
  • - {{/if}} - - {{plugin-outlet "site-map-links"}} - - {{plugin-outlet "site-map-links-last"}} - {{/menu-links}} - - {{mount-widget widget='hamburger-categories' args=(as-hash categories=categories)}} -
    - - {{#menu-links omitRule="true"}} -
  • {{d-link route="about" class="about-link" label="about.simple_title"}}
  • - {{#unless prioritizeFaq}} -
  • {{d-link path=faqUrl class="faq-link" label="faq"}}
  • - {{/unless}} - - {{#if showKeyboardShortcuts}} -
  • {{d-link action="keyboardShortcuts" class="keyboard-shortcuts-link" label="keyboard_shortcuts_help.title"}}
  • - {{/if}} - - {{#if showMobileToggle}} -
  • {{d-link action="toggleMobileView" class="mobile-toggle-link" label=mobileViewLinkTextKey}}
  • - {{/if}} - {{/menu-links}} -{{/menu-panel}} diff --git a/app/assets/javascripts/discourse/templates/components/header-dropdown.hbs b/app/assets/javascripts/discourse/templates/components/header-dropdown.hbs deleted file mode 100644 index 6cb4af742b4..00000000000 --- a/app/assets/javascripts/discourse/templates/components/header-dropdown.hbs +++ /dev/null @@ -1,9 +0,0 @@ - - {{#if showUser}} - {{bound-avatar currentUser "medium"}} - {{else}} - {{fa-icon icon}} - {{/if}} - - -{{yield}} diff --git a/app/assets/javascripts/discourse/templates/components/header-extra-info.hbs b/app/assets/javascripts/discourse/templates/components/header-extra-info.hbs deleted file mode 100644 index 4d5b7987536..00000000000 --- a/app/assets/javascripts/discourse/templates/components/header-extra-info.hbs +++ /dev/null @@ -1,21 +0,0 @@ -
    -
    -
    -

    - {{#if showPrivateMessageGlyph}} - - {{fa-icon "envelope"}} - - {{/if}} - - {{#if topic.details.loaded}} - {{topic-status topic=topic}} - {{{topic.fancyTitle}}} - {{/if}} -

    - {{#if topic.details.loaded}} - {{topic-category topic=topic}} - {{/if}} -
    -
    -
    diff --git a/app/assets/javascripts/discourse/templates/components/menu-links.hbs b/app/assets/javascripts/discourse/templates/components/menu-links.hbs deleted file mode 100644 index fe9b872fcb3..00000000000 --- a/app/assets/javascripts/discourse/templates/components/menu-links.hbs +++ /dev/null @@ -1,7 +0,0 @@ - -{{#unless omitRule}} -
    -{{/unless}} diff --git a/app/assets/javascripts/discourse/templates/components/menu-panel.hbs b/app/assets/javascripts/discourse/templates/components/menu-panel.hbs deleted file mode 100644 index bbb72a5736c..00000000000 --- a/app/assets/javascripts/discourse/templates/components/menu-panel.hbs +++ /dev/null @@ -1,7 +0,0 @@ -{{#if visible}} -
    -
    - {{yield}} -
    -
    -{{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/search-menu.hbs b/app/assets/javascripts/discourse/templates/components/search-menu.hbs deleted file mode 100644 index 1f9cc018a9d..00000000000 --- a/app/assets/javascripts/discourse/templates/components/search-menu.hbs +++ /dev/null @@ -1,40 +0,0 @@ -{{#menu-panel visible=visible onVisible="showedSearch" onHidden="cancelHighlight" maxWidth="500"}} - {{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|}} -
      -
    • {{resultType.name}}
    • - {{component resultType.componentName results=resultType.results term=searchService.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}} -{{/menu-panel}} diff --git a/app/assets/javascripts/discourse/templates/components/search-result-category.hbs b/app/assets/javascripts/discourse/templates/components/search-result-category.hbs deleted file mode 100644 index 071eba18417..00000000000 --- a/app/assets/javascripts/discourse/templates/components/search-result-category.hbs +++ /dev/null @@ -1,7 +0,0 @@ -{{#each results as |result|}} -
  • - - {{category-badge result}} - -
  • -{{/each}} diff --git a/app/assets/javascripts/discourse/templates/components/search-result-post.hbs b/app/assets/javascripts/discourse/templates/components/search-result-post.hbs deleted file mode 100644 index f89090ac24f..00000000000 --- a/app/assets/javascripts/discourse/templates/components/search-result-post.hbs +++ /dev/null @@ -1,14 +0,0 @@ -{{#each results as |result|}} -
  • - - - {{i18n 'search.post_format' post_number=result.post_number username=result.username}} - - {{#unless site.mobileView}} - - {{{unbound result.blurb}}} - - {{/unless}} - -
  • -{{/each}} diff --git a/app/assets/javascripts/discourse/templates/components/search-result-topic.hbs b/app/assets/javascripts/discourse/templates/components/search-result-topic.hbs deleted file mode 100644 index 132e56360b3..00000000000 --- a/app/assets/javascripts/discourse/templates/components/search-result-topic.hbs +++ /dev/null @@ -1,14 +0,0 @@ -{{#each results as |result|}} -
  • - - - {{topic-status topic=result.topic disableActions=true}}{{{unbound result.topic.fancyTitle}}}{{category-badge result.topic.category}}{{plugin-outlet "search-category"}} - - {{#unless site.mobileView}} - - {{format-age result.created_at}} - {{{unbound result.blurb}}} - - {{/unless}} - -
  • -{{/each}} diff --git a/app/assets/javascripts/discourse/templates/components/search-result-user.hbs b/app/assets/javascripts/discourse/templates/components/search-result-user.hbs deleted file mode 100644 index 9cf46a1519f..00000000000 --- a/app/assets/javascripts/discourse/templates/components/search-result-user.hbs +++ /dev/null @@ -1,8 +0,0 @@ -{{#each results as |result|}} -
  • - - {{avatar result imageSize="small"}} - {{unbound result.username}} - -
  • -{{/each}} diff --git a/app/assets/javascripts/discourse/templates/components/user-menu.hbs b/app/assets/javascripts/discourse/templates/components/user-menu.hbs deleted file mode 100644 index 6f014d80d2e..00000000000 --- a/app/assets/javascripts/discourse/templates/components/user-menu.hbs +++ /dev/null @@ -1,48 +0,0 @@ -{{#menu-panel visible=visible}} - - -
    - {{#conditional-loading-spinner condition=loadingNotifications containerClass="spinner-container"}} - {{#if notifications}} -
    -
      - {{#each notifications as |n|}} - {{notification-item notification=n}} - {{/each}} -
    • - {{#d-link path=notificationsPath}} - {{i18n 'notifications.more'}}… - {{/d-link}} -
    • -
    - {{/if}} - {{/conditional-loading-spinner}} -
    - {{plugin-outlet "user-menu-bottom"}} - -{{/menu-panel}} diff --git a/app/assets/javascripts/discourse/templates/composer/similar_topics.hbs b/app/assets/javascripts/discourse/templates/composer/similar-topics.hbs similarity index 64% rename from app/assets/javascripts/discourse/templates/composer/similar_topics.hbs rename to app/assets/javascripts/discourse/templates/composer/similar-topics.hbs index ed326e488e4..257a51ebf11 100644 --- a/app/assets/javascripts/discourse/templates/composer/similar_topics.hbs +++ b/app/assets/javascripts/discourse/templates/composer/similar-topics.hbs @@ -2,5 +2,5 @@

    {{i18n 'composer.similar_topics'}}

      - {{search-result-topic results=similarTopics}} + {{mount-widget widget="search-result-topic" args=(as-hash results=similarTopics)}}
    diff --git a/app/assets/javascripts/discourse/templates/header.hbs b/app/assets/javascripts/discourse/templates/header.hbs deleted file mode 100644 index ad1af735294..00000000000 --- a/app/assets/javascripts/discourse/templates/header.hbs +++ /dev/null @@ -1,66 +0,0 @@ -
    -
    - {{home-logo minimized=showExtraInfo}} - {{plugin-outlet "header-after-home-logo"}} - -
    - {{#unless currentUser}} - {{#if showSignUpButton}} - {{d-button action="showCreateAccount" class="btn-primary btn-small sign-up-button" label="sign_up"}} - {{/if}} - {{d-button action="showLogin" class="btn-primary btn-small login-button" icon="user" label="log_in"}} - {{/unless}} - - {{plugin-outlet "header-before-dropdowns"}} - {{user-menu visible=userMenuVisible logoutAction="logout"}} - {{hamburger-menu visible=hamburgerVisible showKeyboardAction="showKeyboardShortcutsHelp"}} - {{search-menu visible=searchVisible}} -
    - - {{#if showExtraInfo}} - {{header-extra-info topic=topic}} - {{/if}} -
    -
    -{{plugin-outlet "header-under-content"}} diff --git a/app/assets/javascripts/discourse/templates/user/notifications-index.hbs b/app/assets/javascripts/discourse/templates/user/notifications-index.hbs index 9ca0b0b99b1..5d2e184b819 100644 --- a/app/assets/javascripts/discourse/templates/user/notifications-index.hbs +++ b/app/assets/javascripts/discourse/templates/user/notifications-index.hbs @@ -14,14 +14,7 @@
    {{/if}} -{{#each n in model}} -
    - {{notification-item notification=n}} - - {{format-date n.created_at leaveAgo="true"}} - -
    -{{/each}} +{{user-notifications-large notifications=model}} {{#conditional-loading-spinner condition=loading}} {{#unless model.canLoadMore}} diff --git a/app/assets/javascripts/discourse/templates/user/notifications.hbs b/app/assets/javascripts/discourse/templates/user/notifications.hbs index 385e029b6b2..bfdba81bee2 100644 --- a/app/assets/javascripts/discourse/templates/user/notifications.hbs +++ b/app/assets/javascripts/discourse/templates/user/notifications.hbs @@ -23,5 +23,7 @@
    - {{outlet}} + {{#load-more class="notification-history user-stream" selector=".user-stream .notification" action="loadMore"}} + {{outlet}} + {{/load-more}}
    diff --git a/app/assets/javascripts/discourse/views/composer.js.es6 b/app/assets/javascripts/discourse/views/composer.js.es6 index a08ad2c7642..4bc6b5499d6 100644 --- a/app/assets/javascripts/discourse/views/composer.js.es6 +++ b/app/assets/javascripts/discourse/views/composer.js.es6 @@ -1,6 +1,6 @@ import afterTransition from 'discourse/lib/after-transition'; import positioningWorkaround from 'discourse/lib/safari-hacks'; -import { headerHeight } from 'discourse/views/header'; +import { headerHeight } from 'discourse/components/site-header'; import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators'; import Composer from 'discourse/models/composer'; diff --git a/app/assets/javascripts/discourse/views/header.js.es6 b/app/assets/javascripts/discourse/views/header.js.es6 deleted file mode 100644 index eb5ebec50fe..00000000000 --- a/app/assets/javascripts/discourse/views/header.js.es6 +++ /dev/null @@ -1,55 +0,0 @@ -import { on } from 'ember-addons/ember-computed-decorators'; - -export default Ember.View.extend({ - tagName: 'header', - classNames: ['d-header', 'clearfix'], - classNameBindings: ['editingTopic'], - templateName: 'header', - - examineDockHeader() { - // Check the dock after the current run loop. While rendering, - // it's much slower to calculate `outlet.offset()` - Ember.run.next(() => { - if (this.docAt === undefined) { - const outlet = $('#main-outlet'); - if (!(outlet && outlet.length === 1)) return; - this.docAt = outlet.offset().top; - } - - const offset = window.pageYOffset || $('html').scrollTop(); - if (offset >= this.docAt) { - if (!this.dockedHeader) { - $('body').addClass('docked'); - this.dockedHeader = true; - } - } else { - if (this.dockedHeader) { - $('body').removeClass('docked'); - this.dockedHeader = false; - } - } - }); - }, - - @on('willDestroyElement') - _tearDown() { - $(window).unbind('scroll.discourse-dock'); - $(document).unbind('touchmove.discourse-dock'); - this.$('a.unread-private-messages, a.unread-notifications, a[data-notifications]').off('click.notifications'); - $('body').off('keydown.header'); - }, - - @on('didInsertElement') - _setup() { - $(window).bind('scroll.discourse-dock', () => this.examineDockHeader()); - $(document).bind('touchmove.discourse-dock', () => this.examineDockHeader()); - this.examineDockHeader(); - } -}); - -export function headerHeight() { - const $header = $('header.d-header'); - const headerOffset = $header.offset(); - const headerOffsetTop = (headerOffset) ? headerOffset.top : 0; - return parseInt($header.outerHeight() + headerOffsetTop - $(window).scrollTop()); -} diff --git a/app/assets/javascripts/discourse/views/topic.js.es6 b/app/assets/javascripts/discourse/views/topic.js.es6 index 96cdf5826d7..0148b4347bf 100644 --- a/app/assets/javascripts/discourse/views/topic.js.es6 +++ b/app/assets/javascripts/discourse/views/topic.js.es6 @@ -23,6 +23,8 @@ const TopicView = Ember.View.extend(AddCategoryClass, AddArchetypeClass, Scrolli postStream: Em.computed.alias('topic.postStream'), archetype: Em.computed.alias('topic.archetype'), + _lastShowTopic: null, + _composeChanged: function() { const composerController = Discourse.get('router.composerController'); composerController.clearState(); @@ -73,7 +75,7 @@ const TopicView = Ember.View.extend(AddCategoryClass, AddArchetypeClass, Scrolli this.resetExamineDockCache(); // this happens after route exit, stuff could have trickled in - this.set('controller.controllers.header.showExtraInfo', false); + this.appEvents.trigger('header:hide-topic'); }.on('willDestroyElement'), @@ -90,6 +92,14 @@ const TopicView = Ember.View.extend(AddCategoryClass, AddArchetypeClass, Scrolli offset: 0, hasScrolled: Em.computed.gt("offset", 0), + showTopicInHeader(topic, offset) { + if (this.get('docAt')) { + return offset >= this.get('docAt') || topic.get('postStream.firstPostNotLoaded'); + } else { + return topic.get('postStream.firstPostNotLoaded'); + } + }, + // The user has scrolled the window, or it is finished rendering and ready for processing. scrolled() { if (this.isDestroyed || this.isDestroying || this._state !== 'inDOM') { @@ -106,12 +116,16 @@ const TopicView = Ember.View.extend(AddCategoryClass, AddArchetypeClass, Scrolli this.set("offset", offset); - const headerController = this.get('controller.controllers.header'), - topic = this.get('controller.model'); - if (this.get('docAt')) { - headerController.set('showExtraInfo', offset >= this.get('docAt') || topic.get('postStream.firstPostNotLoaded')); - } else { - headerController.set('showExtraInfo', topic.get('postStream.firstPostNotLoaded')); + const topic = this.get('controller.model'); + const showTopic = this.showTopicInHeader(topic, offset); + if (showTopic !== this._lastShowTopic) { + this._lastShowTopic = showTopic; + + if (showTopic) { + this.appEvents.trigger('header:show-topic', topic); + } else { + this.appEvents.trigger('header:hide-topic'); + } } // Trigger a scrolled event diff --git a/app/assets/javascripts/discourse/views/user-notifications.js.es6 b/app/assets/javascripts/discourse/views/user-notifications.js.es6 deleted file mode 100644 index c6741020445..00000000000 --- a/app/assets/javascripts/discourse/views/user-notifications.js.es6 +++ /dev/null @@ -1,6 +0,0 @@ -import LoadMore from "discourse/mixins/load-more"; - -export default Ember.View.extend(LoadMore, { - eyelineSelector: '.user-stream .notification', - classNames: ['user-stream', 'notification-history'] -}); diff --git a/app/assets/javascripts/discourse/widgets/actions-summary.js.es6 b/app/assets/javascripts/discourse/widgets/actions-summary.js.es6 index b776b9a5eb1..55fb1043c8b 100644 --- a/app/assets/javascripts/discourse/widgets/actions-summary.js.es6 +++ b/app/assets/javascripts/discourse/widgets/actions-summary.js.es6 @@ -61,6 +61,7 @@ createWidget('action-link', { createWidget('actions-summary-item', { tagName: 'div.post-action', + buildKey: (attrs) => `actions-summary-item-${attrs.id}`, defaultState() { return { users: [] }; diff --git a/app/assets/javascripts/discourse/widgets/button.js.es6 b/app/assets/javascripts/discourse/widgets/button.js.es6 index 5b4f6e11945..656fc4c1e88 100644 --- a/app/assets/javascripts/discourse/widgets/button.js.es6 +++ b/app/assets/javascripts/discourse/widgets/button.js.es6 @@ -14,6 +14,8 @@ export default createWidget('button', { let title; if (attrs.title) { title = I18n.t(attrs.title, attrs.titleOptions); + } else if (attrs.label) { + title = I18n.t(attrs.label, attrs.labelOptions); } const attributes = { "aria-label": title, title }; diff --git a/app/assets/javascripts/discourse/widgets/click-hook.js.es6 b/app/assets/javascripts/discourse/widgets/click-hook.js.es6 deleted file mode 100644 index 16f1f4124a3..00000000000 --- a/app/assets/javascripts/discourse/widgets/click-hook.js.es6 +++ /dev/null @@ -1,64 +0,0 @@ -/*eslint no-loop-func:0*/ - -const CLICK_ATTRIBUTE_NAME = '_discourse_click_widget'; -const CLICK_OUTSIDE_ATTRIBUTE_NAME = '_discourse_click_outside_widget'; - -export class WidgetClickHook { - constructor(widget) { - this.widget = widget; - } - - hook(node) { - node[CLICK_ATTRIBUTE_NAME] = this.widget; - } - - unhook(node) { - node[CLICK_ATTRIBUTE_NAME] = null; - } -}; - -export class WidgetClickOutsideHook { - constructor(widget) { - this.widget = widget; - } - - hook(node) { - node.setAttribute('data-click-outside', true); - node[CLICK_OUTSIDE_ATTRIBUTE_NAME] = this.widget; - } - - unhook(node) { - node.removeAttribute('data-click-outside'); - node[CLICK_OUTSIDE_ATTRIBUTE_NAME] = null; - } -}; - -let _watchingDocument = false; -WidgetClickHook.setupDocumentCallback = function() { - if (_watchingDocument) { return; } - - $(document).on('click.discourse-widget', e => { - let node = e.target; - while (node) { - const widget = node[CLICK_ATTRIBUTE_NAME]; - if (widget) { - widget.rerenderResult(() => widget.click(e)); - break; - } - node = node.parentNode; - } - - node = e.target; - const $outside = $('[data-click-outside]'); - $outside.each((i, outNode) => { - if (outNode.contains(node)) { return; } - const widget = outNode[CLICK_OUTSIDE_ATTRIBUTE_NAME]; - if (widget) { - widget.clickOutside(e); - } - }); - }); - - - _watchingDocument = true; -}; diff --git a/app/assets/javascripts/discourse/widgets/hamburger-categories.js.es6 b/app/assets/javascripts/discourse/widgets/hamburger-categories.js.es6 index cb86752c36f..ec14630d747 100644 --- a/app/assets/javascripts/discourse/widgets/hamburger-categories.js.es6 +++ b/app/assets/javascripts/discourse/widgets/hamburger-categories.js.es6 @@ -5,7 +5,7 @@ createWidget('hamburger-category', { tagName: 'li.category-link', html(c) { - const results = [ this.attach('category_link', { category: c, allowUncategorized: true }) ]; + const results = [ this.attach('category-link', { category: c, allowUncategorized: true }) ]; const unreadTotal = parseInt(c.get('unreadTopics'), 10) + parseInt(c.get('newTopics'), 10); if (unreadTotal) { diff --git a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 new file mode 100644 index 00000000000..ca9a925b0f3 --- /dev/null +++ b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 @@ -0,0 +1,158 @@ +import { createWidget } from 'discourse/widgets/widget'; +import { h } from 'virtual-dom'; + +export default createWidget('hamburger-menu', { + tagName: 'div.hamburger-panel', + + faqLink(href) { + return h('a.faq-priority', { attributes: { href } }, [ + I18n.t('faq'), + ' ', + h('span.badge.badge-notification', I18n.t('new_item')) + ]); + }, + + adminLinks() { + const { currentUser } = this; + + const links = [{ route: 'admin', className: 'admin-link', icon: 'wrench', label: 'admin_title' }, + { route: 'adminFlags', + className: 'flagged-posts-link', + icon: 'flag', + label: 'flags_title', + badgeClass: 'flagged-posts', + badgeTitle: 'notifications.total_flagged', + badgeCount: 'site_flagged_posts_count' }]; + + if (currentUser.show_queued_posts) { + links.push({ route: 'queued-posts', + className: 'queued-posts-link', + label: 'queue.title', + badgeCount: 'post_queue_new_count', + badgeClass: 'queued-posts' }); + } + + if (currentUser.admin) { + links.push({ route: 'adminSiteSettings', + icon: 'gear', + label: 'admin.site_settings.title', + className: 'settings-link' }); + } + + return links.map(l => this.attach('link', l)); + }, + + lookupCount(type) { + const tts = this.container.lookup('topic-tracking-state:main'); + return tts ? tts.lookupCount(type) : 0; + }, + + showUserDirectory() { + if (!this.siteSettings.enable_user_directory) return false; + if (this.siteSettings.hide_user_profiles_from_public && !this.currentUser) return false; + return true; + }, + + generalLinks() { + const { siteSettings } = this; + const links = []; + + links.push({ route: 'discovery.latest', className: 'latest-topics-link', label: 'filters.latest.title' }); + + if (this.currentUser) { + links.push({ route: 'discovery.new', + className: 'new-topics-link', + labelCount: 'filters.new.title_with_count', + label: 'filters.new.title', + count: this.lookupCount('new') }); + + links.push({ route: 'discovery.unread', + className: 'unread-topics-link', + labelCount: 'filters.unread.title_with_count', + label: 'filters.unread.title', + count: this.lookupCount('unread') }); + } + + links.push({ route: 'discovery.top', className: 'top-topics-link', label: 'filters.top.title' }); + + if (siteSettings.enable_badges) { + links.push({ route: 'badges', className: 'badge-link', label: 'badges.title' }); + } + + if (this.showUserDirectory()) { + links.push({ route: 'users', className: 'user-directory-link', label: 'directory.title' }); + } + + return links.map(l => this.attach('link', l)); + }, + + listCategories() { + const hideUncategorized = !this.siteSettings.allow_uncategorized_topics; + const showSubcatList = this.siteSettings.show_subcategory_list; + const isStaff = Discourse.User.currentProp('staff'); + + const categories = Discourse.Category.list().reject((c) => { + if (showSubcatList && c.get('parent_category_id')) { return true; } + if (hideUncategorized && c.get('isUncategorizedCategory') && !isStaff) { return true; } + return false; + }); + + return this.attach('hamburger-categories', { categories }); + }, + + footerLinks(prioritizeFaq, faqUrl) { + const links = []; + links.push({ route: 'about', className: 'about-link', label: 'about.simple_title' }); + + if (!prioritizeFaq) { + links.push({ href: faqUrl, className: 'faq-link', label: 'faq' }); + } + + const { site } = this; + if (!site.mobileView && !this.capabilities.touch) { + links.push({ action: 'showKeyboard', className: 'keyboard-shortcuts-link', label: 'keyboard_shortcuts_help.title' }); + } + + if (this.site.mobileView || (this.siteSettings.enable_mobile_theme && this.capabilities.touch)) { + links.push({ action: 'toggleMobileView', + className: 'mobile-toggle-link', + label: this.site.mobileView ? "desktop_view" : "mobile_view" }); + } + + return links.map(l => this.attach('link', l)); + }, + + panelContents() { + const { currentUser } = this; + const results = []; + + let faqUrl = this.siteSettings.faq_url; + if (!faqUrl || faqUrl.length === 0) { + faqUrl = Discourse.getURL('/faq'); + } + + const prioritizeFaq = this.currentUser && !this.currentUser.read_faq; + if (prioritizeFaq) { + results.push(this.attach('menu-links', { heading: true, contents: () => this.faqLink(faqUrl) })); + } + + if (currentUser && currentUser.staff) { + results.push(this.attach('menu-links', { contents: () => this.adminLinks() })); + } + + results.push(this.attach('menu-links', { contents: () => this.generalLinks() })); + results.push(this.listCategories()); + results.push(h('hr')); + results.push(this.attach('menu-links', { omitRule: true, contents: () => this.footerLinks(prioritizeFaq, faqUrl) })); + + return results; + }, + + html() { + return this.attach('menu-panel', { contents: () => this.panelContents() }); + }, + + clickOutside() { + this.sendWidgetAction('toggleHamburger'); + } +}); diff --git a/app/assets/javascripts/discourse/widgets/header-topic-info.js.es6 b/app/assets/javascripts/discourse/widgets/header-topic-info.js.es6 new file mode 100644 index 00000000000..94cbf746db4 --- /dev/null +++ b/app/assets/javascripts/discourse/widgets/header-topic-info.js.es6 @@ -0,0 +1,54 @@ +import { createWidget } from 'discourse/widgets/widget'; +import { h } from 'virtual-dom'; +import { iconNode } from 'discourse/helpers/fa-icon'; +import DiscourseURL from 'discourse/lib/url'; + +export default createWidget('header-topic-info', { + tagName: 'div.extra-info-wrapper', + + html(attrs) { + const topic = attrs.topic; + + const heading = []; + + const showPM = !topic.get('is_warning') && topic.get('isPrivateMessage'); + if (showPM) { + const href = this.currentUser && this.currentUser.pmPath(topic); + if (href) { + heading.push(h('a', { attributes: { href } }, + h('span.private-message-glyph', iconNode('envelope')))); + } + } + const loaded = topic.get('details.loaded'); + + if (loaded) { + heading.push(this.attach('topic-status', attrs)); + heading.push(this.attach('link', { className: 'topic-link', + action: 'jumpToTopPost', + href: topic.get('url'), + contents: () => topic.get('fancyTitle') })); + } + + const title = [h('h1', heading)]; + if (loaded) { + const category = topic.get('category'); + if (category && (!category.get('isUncategorizedCategory') || !this.siteSettings.suppress_uncategorized_badge)) { + const parentCategory = category.get('parentCategory'); + if (parentCategory) { + title.push(this.attach('category-link', { category: parentCategory })); + } + title.push(this.attach('category-link', { category })); + } + } + + const contents = h('div.title-wrapper', title); + return h('div.extra-info', { className: title.length > 1 ? 'two-rows' : '' }, contents); + }, + + jumpToTopPost() { + const topic = this.attrs.topic; + if (topic) { + DiscourseURL.routeTo(topic.get('firstPostUrl')); + } + } +}); diff --git a/app/assets/javascripts/discourse/widgets/header.js.es6 b/app/assets/javascripts/discourse/widgets/header.js.es6 new file mode 100644 index 00000000000..314acd1ec78 --- /dev/null +++ b/app/assets/javascripts/discourse/widgets/header.js.es6 @@ -0,0 +1,278 @@ +import { createWidget } from 'discourse/widgets/widget'; +import { iconNode } from 'discourse/helpers/fa-icon'; +import { avatarImg } from 'discourse/widgets/post'; +import DiscourseURL from 'discourse/lib/url'; + +import { h } from 'virtual-dom'; + +const dropdown = { + buildClasses(attrs) { + if (attrs.active) { return "active"; } + }, + + click(e) { + e.preventDefault(); + if (!this.attrs.active) { + this.sendWidgetAction(this.attrs.action); + } + } +}; + +createWidget('header-notifications', { + html(attrs) { + const { currentUser } = this; + + const contents = [ avatarImg('medium', { template: currentUser.get('avatar_template'), + username: currentUser.get('username') }) ]; + + const unreadNotifications = currentUser.get('unread_notifications'); + if (!!unreadNotifications) { + contents.push(this.attach('link', { action: attrs.action, + className: 'badge-notification unread-notifications', + rawLabel: unreadNotifications })); + } + + const unreadPMs = currentUser.get('unread_private_messages'); + if (!!unreadPMs) { + contents.push(this.attach('link', { action: attrs.action, + className: 'badge-notification unread-private-messages', + rawLabel: unreadPMs })); + } + + return contents; + } +}); + +createWidget('user-dropdown', jQuery.extend(dropdown, { + tagName: 'li.header-dropdown-toggle.current-user', + + buildId() { + return 'current-user'; + }, + + html(attrs) { + const { currentUser } = this; + + return h('a.icon', { attributes: { href: currentUser.get('path'), 'data-auto-route': true } }, + this.attach('header-notifications', attrs)); + } +})); + +createWidget('header-dropdown', jQuery.extend(dropdown, { + tagName: 'li.header-dropdown-toggle', + + html(attrs) { + const title = I18n.t(attrs.title); + + const body = [iconNode(attrs.icon)]; + if (attrs.contents) { + body.push(attrs.contents.call(this)); + } + + return h('a.icon', { attributes: { href: '', + 'data-auto-route': true, + title, + 'aria-label': title, + id: attrs.iconId } }, body); + } +})); + +createWidget('header-icons', { + tagName: 'ul.icons.clearfix', + + buildAttributes() { + return { role: 'navigation' }; + }, + + html(attrs) { + const hamburger = this.attach('header-dropdown', { + title: 'hamburger_menu', + icon: 'bars', + iconId: 'toggle-hamburger-menu', + active: attrs.hamburgerVisible, + action: 'toggleHamburger', + contents() { + if (!attrs.flagCount) { return; } + return this.attach('link', { + href: '/admin/flags/active', + title: 'notifications.total_flagged', + rawLabel: attrs.flagCount, + className: 'badge-notification flagged-posts' + }); + } + }); + + const search = this.attach('header-dropdown', { + title: 'search.title', + icon: 'search', + iconId: 'search-button', + action: 'toggleSearchMenu', + active: attrs.searchVisible, + href: '/search' + }); + + const icons = [search, hamburger]; + if (this.currentUser) { + icons.push(this.attach('user-dropdown', { active: attrs.userVisible, + action: 'toggleUserMenu' })); + } + + return icons; + }, +}); + +createWidget('header-buttons', { + tagName: 'span', + + html(attrs) { + if (this.currentUser) { return; } + + const buttons = []; + + if (attrs.canSignUp && !attrs.topic) { + buttons.push(this.attach('button', { label: "sign_up", + className: 'btn-primary btn-small sign-up-button', + action: "showCreateAccount" })); + } + + + buttons.push(this.attach('button', { label: 'log_in', + className: 'btn-primary btn-small login-button', + action: 'showLogin', + icon: 'user' })); + return buttons; + } +}); + +export default createWidget('header', { + tagName: 'header.d-header.clearfix', + buildKey: () => `header`, + + defaultState() { + return { searchVisible: false, + hamburgerVisible: false, + userVisible: false, + contextEnabled: false }; + }, + + html(attrs, state) { + const panels = [this.attach('header-buttons', attrs), + this.attach('header-icons', { hamburgerVisible: state.hamburgerVisible, + userVisible: state.userVisible, + searchVisible: state.searchVisible, + flagCount: attrs.flagCount })]; + + if (state.searchVisible) { + panels.push(this.attach('search-menu', { contextEnabled: state.contextEnabled })); + } else if (state.hamburgerVisible) { + panels.push(this.attach('hamburger-menu')); + } else if (state.userVisible) { + panels.push(this.attach('user-menu')); + } + + const contents = [ this.attach('home-logo', { minimized: !!attrs.topic }), + h('div.panel.clearfix', panels) ]; + + if (attrs.topic) { + contents.push(this.attach('header-topic-info', attrs)); + } + + return h('div.wrap', h('div.contents.clearfix', contents)); + }, + + updateHighlight() { + if (!this.state.searchVisible) { + const service = this.container.lookup('search-service:main'); + service.set('highlightTerm', ''); + } + }, + + linkClickedEvent() { + this.state.userVisible = false; + this.state.hamburgerVisible = false; + this.state.searchVisible = false; + this.updateHighlight(); + }, + + toggleSearchMenu() { + if (this.site.mobileView) { + const searchService = this.container.lookup('search-service:main'); + const context = searchService.get('searchContext'); + var params = ""; + + if (context) { + params = `?context=${context.type}&context_id=${context.id}&skip_context=true`; + } + + return DiscourseURL.routeTo('/search' + params); + } + + this.state.searchVisible = !this.state.searchVisible; + this.updateHighlight(); + Ember.run.next(() => $('#search-term').focus()); + }, + + toggleUserMenu() { + this.state.userVisible = !this.state.userVisible; + }, + + toggleHamburger() { + this.state.hamburgerVisible = !this.state.hamburgerVisible; + }, + + togglePageSearch() { + const { state } = this; + + if (state.searchVisible) { + this.toggleSearchMenu(); + return false; + } + + state.contextEnabled = false; + + const currentPath = this.container.lookup('controller:application').get('currentPath'); + const blacklist = [ /^discovery\.categories/ ]; + const whitelist = [ /^topic\./ ]; + const check = function(regex) { return !!currentPath.match(regex); }; + let showSearch = whitelist.any(check) && !blacklist.any(check); + + // If we're viewing a topic, only intercept search if there are cloaked posts + if (showSearch && currentPath.match(/^topic\./)) { + showSearch = ($('.topic-post .cooked, .small-action:not(.time-gap)').length < + this.container.lookup('controller:topic').get('model.postStream.stream.length')); + } + + if (showSearch) { + state.contextEnabled = true; + this.toggleSearchMenu(); + return false; + } + + return true; + }, + + searchMenuContextChanged(value) { + this.state.contextEnabled = value; + }, + + headerKeyboardTrigger(msg) { + switch(msg.type) { + case 'search': + this.toggleSearchMenu(); + break; + case 'user': + this.toggleUserMenu(); + break; + case 'hamburger': + this.toggleHamburger(); + break; + case 'page-search': + if (!this.togglePageSearch()) { + msg.event.preventDefault(); + msg.event.stopPropagation(); + } + break; + } + } + +}); diff --git a/app/assets/javascripts/discourse/widgets/home-logo.js.es6 b/app/assets/javascripts/discourse/widgets/home-logo.js.es6 new file mode 100644 index 00000000000..1a2f01f40a7 --- /dev/null +++ b/app/assets/javascripts/discourse/widgets/home-logo.js.es6 @@ -0,0 +1,48 @@ +import DiscourseURL from 'discourse/lib/url'; +import { createWidget } from 'discourse/widgets/widget'; +import { h } from 'virtual-dom'; +import { iconNode } from 'discourse/helpers/fa-icon'; + +export default createWidget('home-logo', { + tagName: 'div.title', + + logo() { + const { siteSettings } = this; + const mobileView = this.site.mobileView; + + const mobileLogoUrl = siteSettings.mobile_logo_url || ""; + const showMobileLogo = mobileView && (mobileLogoUrl.length > 0); + + const logoUrl = siteSettings.logo_url || ''; + const title = siteSettings.title; + + if (!mobileView && this.attrs.minimized) { + const logoSmallUrl = siteSettings.logo_small_url || ''; + if (logoSmallUrl.length) { + return h('img#site-logo.logo-small', { attributes: { src: logoSmallUrl, width: 33, height: 33, alt: title } }); + } else { + return iconNode('home'); + } + } else if (showMobileLogo) { + return h('img#site-logo.logo-big', { attributes: { src: mobileLogoUrl, alt: title } }); + } else if (logoUrl.length) { + return h('img#site-logo.logo-big', { attributes: { src: logoUrl, alt: title } }); + } else { + return h('h2#site-text-logo.text-logo', title); + } + }, + + html() { + return h('a', { attributes: { href: "/", 'data-auto-route': true } }, this.logo()); + }, + + click(e) { + // if they want to open in a new tab, let it so + if (e.shiftKey || e.metaKey || e.ctrlKey || e.which === 2) { return true; } + + e.preventDefault(); + + DiscourseURL.routeTo("/"); + return false; + } +}); diff --git a/app/assets/javascripts/discourse/widgets/hooks.js.es6 b/app/assets/javascripts/discourse/widgets/hooks.js.es6 new file mode 100644 index 00000000000..669eac77b5c --- /dev/null +++ b/app/assets/javascripts/discourse/widgets/hooks.js.es6 @@ -0,0 +1,67 @@ +/*eslint no-loop-func:0*/ + +const CLICK_ATTRIBUTE_NAME = '_discourse_click_widget'; +const CLICK_OUTSIDE_ATTRIBUTE_NAME = '_discourse_click_outside_widget'; +const KEY_UP_ATTRIBUTE_NAME = '_discourse_key_up_widget'; + +function buildHook(attributeName, setAttr) { + return class { + constructor(widget) { + this.widget = widget; + } + + hook(node) { + if (setAttr) { + node.setAttribute(setAttr, true); + } + node[attributeName] = this.widget; + } + + unhook(node) { + if (setAttr) { + node.removeAttribute(setAttr, true); + } + node[attributeName] = null; + } + }; +} + +export const WidgetClickHook = buildHook(CLICK_ATTRIBUTE_NAME); +export const WidgetClickOutsideHook = buildHook(CLICK_OUTSIDE_ATTRIBUTE_NAME, 'data-click-outside'); +export const WidgetKeyUpHook = buildHook(KEY_UP_ATTRIBUTE_NAME); + +function findNode(node, attrName, cb) { + while (node) { + const widget = node[attrName]; + if (widget) { + widget.rerenderResult(() => cb(widget)); + break; + } + node = node.parentNode; + } +} + +let _watchingDocument = false; +WidgetClickHook.setupDocumentCallback = function() { + if (_watchingDocument) { return; } + + $(document).on('click.discourse-widget', e => { + findNode(e.target, CLICK_ATTRIBUTE_NAME, w => w.click(e)); + + let node = e.target; + const $outside = $('[data-click-outside]'); + $outside.each((i, outNode) => { + if (outNode.contains(node)) { return; } + const widget = outNode[CLICK_OUTSIDE_ATTRIBUTE_NAME]; + if (widget) { + widget.clickOutside(e); + } + }); + }); + + $(document).on('keyup.discourse-widget', e => { + findNode(e.target, KEY_UP_ATTRIBUTE_NAME, w => w.keyUp(e)); + }); + + _watchingDocument = true; +}; diff --git a/app/assets/javascripts/discourse/widgets/link.js.es6 b/app/assets/javascripts/discourse/widgets/link.js.es6 new file mode 100644 index 00000000000..49ccb07a56c --- /dev/null +++ b/app/assets/javascripts/discourse/widgets/link.js.es6 @@ -0,0 +1,84 @@ +import { createWidget } from 'discourse/widgets/widget'; +import { iconNode } from 'discourse/helpers/fa-icon'; +import { h } from 'virtual-dom'; +import DiscourseURL from 'discourse/lib/url'; + +export default createWidget('link', { + tagName: 'a', + + href(attrs) { + const route = attrs.route; + if (route) { + const router = this.container.lookup('router:main'); + if (router && router.router) { + const params = [route]; + if (attrs.model) { + params.push(attrs.model); + } + return Discourse.getURL(router.router.generate.apply(router.router, params)); + } + } else { + return attrs.href; + } + }, + + buildClasses(attrs) { + const result = []; + result.push('widget-link'); + if (attrs.className) { result.push(attrs.className); }; + return result; + }, + + buildAttributes(attrs) { + return { href: this.href(attrs), title: this.label(attrs) }; + }, + + label(attrs) { + if (attrs.labelCount && attrs.count) { + return I18n.t(attrs.labelCount, { count: attrs.count }); + } + return attrs.rawLabel || (attrs.label ? I18n.t(attrs.label) : ''); + }, + + html(attrs) { + if (attrs.contents) { + return attrs.contents(); + } + + const result = []; + if (attrs.icon) { + result.push(iconNode(attrs.icon)); + result.push(' '); + } + + if (!attrs.hideLabel) { + result.push(this.label(attrs)); + } + + const currentUser = this.currentUser; + if (currentUser && attrs.badgeCount) { + const val = parseInt(currentUser.get(attrs.badgeCount)); + if (val > 0) { + const title = attrs.badgeTitle ? I18n.t(attrs.badgeTitle) : ''; + result.push(' '); + result.push(h('span.badge-notification', { className: attrs.badgeClass, + attributes: { title } }, val)); + } + } + + return result; + }, + + click(e) { + e.preventDefault(); + + if (this.attrs.action) { + e.preventDefault(); + return this.sendWidgetAction(this.attrs.action, this.attrs.actionParam); + } else { + this.sendWidgetEvent('linkClicked'); + } + + return DiscourseURL.routeTo(this.href(this.attrs)); + } +}); diff --git a/app/assets/javascripts/discourse/widgets/menu-panel.js.es6 b/app/assets/javascripts/discourse/widgets/menu-panel.js.es6 new file mode 100644 index 00000000000..8a1ede9663c --- /dev/null +++ b/app/assets/javascripts/discourse/widgets/menu-panel.js.es6 @@ -0,0 +1,32 @@ +import { createWidget } from 'discourse/widgets/widget'; +import { h } from 'virtual-dom'; + +createWidget('menu-links', { + html(attrs) { + const links = [].concat(attrs.contents()); + const liOpts = { className: attrs.heading ? 'heading' : '' }; + + const result = []; + result.push(h('ul.menu-links.columned', links.map(l => h('li', liOpts, l)))); + + result.push(h('div.clearfix')); + if (!attrs.omitRule) { + result.push(h('hr')); + } + return result; + } +}); + +createWidget('menu-panel', { + tagName: 'div.menu-panel', + + buildAttributes(attrs) { + if (attrs.maxWidth) { + return { 'data-max-width': attrs.maxWidth }; + } + }, + + html(attrs) { + return h('div.panel-body', h('div.panel-body-contents.clearfix', attrs.contents())); + } +}); diff --git a/app/assets/javascripts/discourse/widgets/notification-item.js.es6 b/app/assets/javascripts/discourse/widgets/notification-item.js.es6 new file mode 100644 index 00000000000..108e757b888 --- /dev/null +++ b/app/assets/javascripts/discourse/widgets/notification-item.js.es6 @@ -0,0 +1,109 @@ +import RawHtml from 'discourse/widgets/raw-html'; +import { createWidget } from 'discourse/widgets/widget'; +import DiscourseURL from 'discourse/lib/url'; +import { h } from 'virtual-dom'; + +const LIKED_TYPE = 5; +const INVITED_TYPE = 8; +const GROUP_SUMMARY_TYPE = 16; + +createWidget('notification-item', { + tagName: 'li', + + buildClasses(attrs) { + const classNames = []; + if (attrs.get('read')) { classNames.push('read'); } + if (attrs.is_warning) { classNames.push('is-warning'); } + return classNames; + }, + + url() { + const attrs = this.attrs; + const data = attrs.data; + + const badgeId = data.badge_id; + if (badgeId) { + let badgeSlug = data.badge_slug; + + if (!badgeSlug) { + const badgeName = data.badge_name; + badgeSlug = badgeName.replace(/[^A-Za-z0-9_]+/g, '-').toLowerCase(); + } + + let username = data.username; + username = username ? "?username=" + username.toLowerCase() : ""; + return Discourse.getURL('/badges/' + badgeId + '/' + badgeSlug + username); + } + + const topicId = attrs.topic_id; + if (topicId) { + return Discourse.Utilities.postUrl(attrs.slug, topicId, attrs.post_number); + } + + if (attrs.notification_type === INVITED_TYPE) { + return Discourse.getURL('/users/' + data.display_username); + } + + if (data.group_id) { + return Discourse.getURL('/users/' + data.username + '/messages/group/' + data.group_name); + } + }, + + description() { + const data = this.attrs.data; + const badgeName = data.badge_name; + if (badgeName) { return Discourse.Utilities.escapeExpression(badgeName); } + + const title = data.topic_title; + return Ember.isEmpty(title) ? "" : Discourse.Utilities.escapeExpression(title); + }, + + text() { + const attrs = this.attrs; + const data = attrs.data; + + const notificationType = attrs.notification_type; + + const lookup = this.site.get('notificationLookup'); + const notName = lookup[notificationType]; + const scope = (notName === 'custom') ? data.message : `notifications.${notName}`; + + if (notificationType === GROUP_SUMMARY_TYPE) { + const count = data.inbox_count; + const group_name = data.group_name; + return I18n.t(scope, { count, group_name }); + } + + const username = data.display_username; + const description = this.description(); + if (notificationType === LIKED_TYPE && data.count > 1) { + const count = data.count - 2; + const username2 = data.username2; + if (count===0) { + return I18n.t('notifications.liked_2', {description, username, username2}); + } else { + return I18n.t('notifications.liked_many', {description, username, username2, count}); + } + } + + return I18n.t(scope, {description, username}); + }, + + html() { + const contents = new RawHtml({ html: `
    ${Discourse.Emoji.unescape(this.text())}
    ` }); + const url = this.url(); + return url ? h('a', { attributes: { href: url, 'data-auto-route': true } }, contents) : contents; + }, + + click(e) { + e.preventDefault(); + this.attrs.set('read', true); + const id = this.attrs.id; + Discourse.setTransientHeader("Discourse-Clear-Notifications", id); + if (document && document.cookie) { + document.cookie = `cn=${id}; expires=Fri, 31 Dec 9999 23:59:59 GMT`; + } + this.sendWidgetEvent('linkClicked'); + DiscourseURL.routeTo(this.url()); + } +}); diff --git a/app/assets/javascripts/discourse/widgets/post-gutter.js.es6 b/app/assets/javascripts/discourse/widgets/post-gutter.js.es6 index 61d7f973e0f..81443e10bcf 100644 --- a/app/assets/javascripts/discourse/widgets/post-gutter.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post-gutter.js.es6 @@ -7,6 +7,7 @@ const MAX_GUTTER_LINKS = 5; export default createWidget('post-gutter', { tagName: 'div.gutter', + buildKey: (attrs) => `post-gutter-${attrs.id}`, defaultState() { return { collapsed: true }; diff --git a/app/assets/javascripts/discourse/widgets/post.js.es6 b/app/assets/javascripts/discourse/widgets/post.js.es6 index 0cf321bcfe6..7dcd728c57a 100644 --- a/app/assets/javascripts/discourse/widgets/post.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post.js.es6 @@ -51,6 +51,7 @@ createWidget('select-post', { createWidget('reply-to-tab', { tagName: 'a.reply-to-tab', + buildKey: attrs => `reply-to-tab-${attrs.id}`, defaultState() { return { loading: false }; @@ -61,7 +62,7 @@ createWidget('reply-to-tab', { return [iconNode('mail-forward'), ' ', - avatarImg.call(this,'small',{ + avatarImg('small', { template: attrs.replyToAvatarTemplate, username: attrs.replyToUsername }), diff --git a/app/assets/javascripts/discourse/widgets/raw-html.js.es6 b/app/assets/javascripts/discourse/widgets/raw-html.js.es6 index a1eddd74b15..e009d3caf43 100644 --- a/app/assets/javascripts/discourse/widgets/raw-html.js.es6 +++ b/app/assets/javascripts/discourse/widgets/raw-html.js.es6 @@ -4,9 +4,13 @@ export default class RawHtml { } init() { - return $(this.html)[0]; + const $html = $(this.html); + this.decorate($html); + return $html[0]; } + decorate() { } + update(prev) { if (prev.html === this.html) { return; } return this.init(); diff --git a/app/assets/javascripts/discourse/widgets/search-menu-controls.js.es6 b/app/assets/javascripts/discourse/widgets/search-menu-controls.js.es6 new file mode 100644 index 00000000000..61e0626f8d8 --- /dev/null +++ b/app/assets/javascripts/discourse/widgets/search-menu-controls.js.es6 @@ -0,0 +1,60 @@ +import { searchContextDescription } from 'discourse/lib/search'; +import { h } from 'virtual-dom'; +import { createWidget } from 'discourse/widgets/widget'; + +createWidget('search-term', { + tagName: 'input', + buildId: () => 'search-term', + + buildAttributes(attrs) { + return { type: 'text', + value: attrs.value || '', + placeholder: attrs.contextEnabled ? "" : I18n.t('search.title') }; + }, + + keyUp(e) { + if (e.which === 13) { + return this.sendWidgetAction('fullSearch'); + } + + const val = this.attrs.value; + const newVal = $(`#${this.buildId()}`).val(); + + if (newVal !== val) { + this.sendWidgetAction('searchTermChanged', newVal); + } + } +}); + +createWidget('search-context', { + tagName: 'div.search-context', + + html(attrs) { + const service = this.container.lookup('search-service:main'); + const ctx = service.get('searchContext'); + + const result = []; + if (ctx) { + const description = searchContextDescription(Ember.get(ctx, 'type'), + Ember.get(ctx, 'user.username') || Ember.get(ctx, 'category.name')); + result.push(h('label', [ + h('input', { type: 'checkbox', checked: attrs.contextEnabled }), + ' ', + description + ])); + } + + result.push(this.attach('link', { action: 'showSearchHelp', + label: 'show_help', + className: 'show-help' })); + result.push(h('div.clearfix')); + return result; + }, + + click() { + const val = $('.search-context input').is(':checked'); + if (val !== this.attrs.contextEnabled) { + this.sendWidgetAction('searchContextChanged', val); + } + } +}); diff --git a/app/assets/javascripts/discourse/widgets/search-menu-results.js.es6 b/app/assets/javascripts/discourse/widgets/search-menu-results.js.es6 new file mode 100644 index 00000000000..5d6e03a82c0 --- /dev/null +++ b/app/assets/javascripts/discourse/widgets/search-menu-results.js.es6 @@ -0,0 +1,100 @@ +import { avatarImg } from 'discourse/widgets/post'; +import { dateNode } from 'discourse/helpers/node'; +import RawHtml from 'discourse/widgets/raw-html'; +import { createWidget } from 'discourse/widgets/widget'; +import { h } from 'virtual-dom'; +import { iconNode } from 'discourse/helpers/fa-icon'; + +class Highlighted extends RawHtml { + constructor(html, term) { + super({ html: `${html}` }); + this.term = term; + } + + decorate($html) { + $html.highlight(this.term.split(/\s+/), { className: 'search-highlight' }); + } +} + +function createSearchResult(type, linkField, fn) { + return createWidget(`search-result-${type}`, { + html(attrs) { + return attrs.results.map(r => { + return h('li', this.attach('link', { + href: r.get(linkField), + contents: () => fn.call(this, r, attrs.term), + className: 'search-link' + })); + }); + } + }); +} + +function postResult(result, link, term) { + const html = [link]; + + if (!this.site.mobileView) { + html.push(h('span.blurb', [ dateNode(result.created_at), + ' - ', + new Highlighted(result.blurb, term) ])); + } + + return html; +} + +createSearchResult('user', 'path', function(u) { + return [ avatarImg('small', { template: u.avatar_template, username: u.username }), ' ', u.username ]; +}); + +createSearchResult('topic', 'url', function(result, term) { + const topic = result.topic; + const link = h('span.topic', [ + this.attach('topic-status', { topic, disableActions: true }), + h('span.topic-title', new Highlighted(topic.get('fancyTitle'), term)), + this.attach('category-link', { category: topic.get('category'), link: false }) + ]); + + return postResult.call(this, result, link, term); +}); + +createSearchResult('post', 'url', function(result, term) { + return postResult.call(this, result, I18n.t('search.post_format', result), term); +}); + +createSearchResult('category', 'url', function (c) { + return this.attach('category-link', { category: c, link: false }); +}); + +createWidget('search-menu-results', { + tagName: 'div.results', + + html(attrs) { + if (attrs.noResults) { + return h('div.no-results', I18n.t('search.no_results')); + } + + const results = attrs.results; + const resultTypes = results.resultTypes || []; + return resultTypes.map(rt => { + const more = []; + + const moreArgs = { + className: 'filter', + contents: () => [I18n.t('show_more'), ' ', iconNode('chevron-down')] + }; + + if (rt.moreUrl) { + more.push(this.attach('link', $.extend(moreArgs, { href: rt.moreUrl }))); + } else if (rt.more) { + more.push(this.attach('link', $.extend(moreArgs, { action: "moreOfType", + actionParam: rt.type, + className: "filter filter-type"}))); + } + + return [ + h('ul', this.attach(rt.componentName, { results: rt.results, term: attrs.term })), + h('div.no-results', more) + ]; + }); + } +}); diff --git a/app/assets/javascripts/discourse/widgets/search-menu.js.es6 b/app/assets/javascripts/discourse/widgets/search-menu.js.es6 new file mode 100644 index 00000000000..c03f1a99e5a --- /dev/null +++ b/app/assets/javascripts/discourse/widgets/search-menu.js.es6 @@ -0,0 +1,166 @@ +import { searchForTerm, isValidSearchTerm } from 'discourse/lib/search'; +import { createWidget } from 'discourse/widgets/widget'; +import { h } from 'virtual-dom'; +import DiscourseURL from 'discourse/lib/url'; + +// Helps with debouncing and cancelling promises +const SearchHelper = { + _activeSearch: null, + _cancelSearch: null, + + // for cancelling debounced search + cancel() { + if (this._activeSearch) { + this._activeSearch.abort(); + } + + this._cancelSearch = true; + Ember.run.later(() => this._cancelSearch = false, 400); + }, + + perform(widget) { + if (this._cancelSearch){ + this._cancelSearch = null; + return; + } + + if (this._activeSearch) { + this._activeSearch.abort(); + this._activeSearch = null; + } + + const { state } = widget; + const { term, typeFilter, contextEnabled } = state; + const searchContext = contextEnabled ? widget.searchContext() : null; + const fullSearchUrl = widget.fullSearchUrl(); + + this._activeSearch = searchForTerm(term, { typeFilter, searchContext, fullSearchUrl }); + this._activeSearch.then(content => { + state.noResults = content.resultTypes.length === 0; + state.results = content; + }).finally(() => { + state.loading = false; + widget.scheduleRerender(); + this._activeSearch = null; + }); + } +}; + +export default createWidget('search-menu', { + tagName: 'div.search-menu', + buildKey: () => 'search-menu', + + defaultState() { + return { loading: false, + results: {}, + noResults: false, + term: null, + typeFilter: null }; + }, + + fullSearchUrl() { + const state = this.state; + const contextEnabled = this.attrs.contextEnabled; + + const ctx = contextEnabled ? this.searchContext() : null; + const type = Ember.get(ctx, 'type'); + + if (contextEnabled && type === 'topic') { + return; + } + + let url = '/search?q=' + encodeURIComponent(state.term); + if (contextEnabled) { + if (ctx.id.toString().toLowerCase() === this.currentUser.username_lower && + type === "private_messages") { + url += ' in:private'; + } else { + url += encodeURIComponent(" " + type + ":" + ctx.id); + } + } + + return Discourse.getURL(url); + }, + + panelContents() { + const { state } = this; + const contextEnabled = this.attrs.contextEnabled; + + const results = [this.attach('search-term', { value: state.term, contextEnabled }), + this.attach('search-context', { contextEnabled })]; + + if (state.loading) { + results.push(h('div.searching', h('div.spinner'))); + } else { + results.push(this.attach('search-menu-results', { term: state.term, + noResults: state.noResults, + results: state.results })); + } + + return results; + }, + + searchService() { + if (!this._searchService) { + this._searchService = this.container.lookup('search-service:main'); + } + return this._searchService; + }, + + searchContext() { + if (!this._searchContext) { + this._searchContext = this.searchService().get('searchContext'); + } + return this._searchContext; + }, + + html() { + return this.attach('menu-panel', { maxWidth: 500, contents: () => this.panelContents() }); + }, + + clickOutside() { + this.sendWidgetAction('toggleSearchMenu'); + }, + + triggerSearch() { + const { state } = this; + + state.noResults = false; + if (isValidSearchTerm(state.term)) { + this.searchService().set('highlightTerm', state.term); + state.loading = true; + Ember.run.debounce(SearchHelper, SearchHelper.perform, this, 400); + } else { + state.results = []; + } + }, + + moreOfType(type) { + this.state.typeFilter = type; + this.triggerSearch(); + }, + + searchContextChanged(enabled) { + this.state.typeFilter = null; + this.sendWidgetAction('searchMenuContextChanged', enabled); + this.state.contextEnabled = enabled; + this.triggerSearch(); + }, + + searchTermChanged(term) { + this.state.typeFilter = null; + this.state.term = term; + this.triggerSearch(); + }, + + fullSearch() { + if (!isValidSearchTerm(this.state.term)) { return; } + + SearchHelper.cancel(); + const url = this.fullSearchUrl(); + if (url) { + this.sendWidgetEvent('linkClicked'); + DiscourseURL.routeTo(url); + } + } +}); diff --git a/app/assets/javascripts/discourse/widgets/topic-map.js.es6 b/app/assets/javascripts/discourse/widgets/topic-map.js.es6 index 53e1fadde91..7c7be61d6e5 100644 --- a/app/assets/javascripts/discourse/widgets/topic-map.js.es6 +++ b/app/assets/javascripts/discourse/widgets/topic-map.js.es6 @@ -125,6 +125,7 @@ createWidget('topic-map-link', { createWidget('topic-map-expanded', { tagName: 'section.topic-map-expanded', + buildKey: attrs => `topic-map-expanded-${attrs.id}`, defaultState() { return { allLinksShown: false }; diff --git a/app/assets/javascripts/discourse/widgets/topic-status.js.es6 b/app/assets/javascripts/discourse/widgets/topic-status.js.es6 new file mode 100644 index 00000000000..335fa5560d6 --- /dev/null +++ b/app/assets/javascripts/discourse/widgets/topic-status.js.es6 @@ -0,0 +1,39 @@ +import { createWidget } from 'discourse/widgets/widget'; +import { iconNode } from 'discourse/helpers/fa-icon'; +import { h } from 'virtual-dom'; + +function renderIcon(name, key, canAct) { + const iconArgs = key === 'unpinned' ? { 'class': 'unpinned' } : null, + icon = iconNode(name, iconArgs); + + const attributes = { title: Discourse.Utilities.escapeExpression(I18n.t(`topic_statuses.${key}.help`)) }; + return h(`${canAct ? 'a' : 'span'}.topic-status`, attributes, icon); +} + +export default createWidget('topic-status', { + html(attrs) { + const topic = attrs.topic; + const canAct = this.currentUser && !attrs.disableActions; + + const result = []; + const renderIconIf = (conditionProp, name, key) => { + if (!topic.get(conditionProp)) { return; } + result.push(renderIcon(name, key, canAct)); + }; + + renderIconIf('is_warning', 'envelope', 'warning'); + + if (topic.get('closed') && topic.get('archived')) { + renderIcon('lock', 'locked_and_archived'); + } else { + renderIconIf('topic.closed', 'lock', 'locked'); + renderIconIf('topic.archived', 'lock', 'archived'); + } + + renderIconIf('topic.pinned', 'thumb-tack', 'pinned'); + renderIconIf('topic.unpinned', 'thumb-tack', 'unpinned'); + renderIconIf('topic.invisible', 'eye-slash', 'invisible'); + + return result; + } +}); diff --git a/app/assets/javascripts/discourse/widgets/user-menu.js.es6 b/app/assets/javascripts/discourse/widgets/user-menu.js.es6 new file mode 100644 index 00000000000..da2c8504f4f --- /dev/null +++ b/app/assets/javascripts/discourse/widgets/user-menu.js.es6 @@ -0,0 +1,93 @@ +import { createWidget } from 'discourse/widgets/widget'; +import { h } from 'virtual-dom'; + +createWidget('user-menu-links', { + tagName: 'div.menu-links-header', + + html(attrs) { + const { currentUser, siteSettings } = this; + + const isAnon = currentUser.is_anonymous; + const allowAnon = siteSettings.allow_anonymous_posting && + currentUser.trust_level >= siteSettings.anonymous_posting_min_trust_level || + isAnon; + + const path = attrs.path; + const glyphs = [{ label: 'user.bookmarks', + className: 'user-bookmarks-link', + icon: 'bookmark', + href: `${path}/activity/bookmarks` }]; + + if (siteSettings.enable_private_messages) { + glyphs.push({ label: 'user.private_messages', + className: 'user-pms-link', + icon: 'envelope', + href: `${path}/messages` }); + } + + const profileLink = { + route: 'user', + model: currentUser, + className: 'user-activity-link', + icon: 'user', + rawLabel: currentUser.username + }; + + if (currentUser.is_anonymous) { + profileLink.label = 'user.profile'; + profileLink.rawLabel = null; + } + + const links = [profileLink]; + if (allowAnon) { + if (!isAnon) { + glyphs.push({ action: 'toggleAnonymous', + label: 'switch_to_anon', + className: 'enable-anonymous', + icon: 'user-secret' }); + } else { + links.push({ className: 'disable-anonymous', + action: 'toggleAnonymous', + label: 'switch_from_anon' }); + } + } + + // preferences always goes last + glyphs.push({ label: 'user.preferences', + className: 'user-preferences-link', + icon: 'gear', + href: `${path}/preferences` }); + + return h('ul.menu-links-row', [ + links.map(l => h('li', this.attach('link', l))), + h('li.glyphs', glyphs.map(l => this.attach('link', $.extend(l, { hideLabel: true })))), + ]); + } +}); + +export default createWidget('user-menu', { + tagName: 'div.user-menu', + + panelContents() { + const path = this.currentUser.get('path'); + + return [this.attach('user-menu-links', { path }), + this.attach('user-notifications', { path }), + h('div.logout-link', [ + h('hr'), + h('ul.menu-links', + h('li', this.attach('link', { action: 'logout', + className: 'logout', + icon: 'sign-out', + label: 'user.log_out' }))) + ])]; + }, + + html() { + return this.attach('menu-panel', { contents: () => this.panelContents() }); + }, + + clickOutside() { + this.sendWidgetAction('toggleUserMenu'); + } +}); diff --git a/app/assets/javascripts/discourse/widgets/user-notifications-large.js.es6 b/app/assets/javascripts/discourse/widgets/user-notifications-large.js.es6 new file mode 100644 index 00000000000..d10e64b5818 --- /dev/null +++ b/app/assets/javascripts/discourse/widgets/user-notifications-large.js.es6 @@ -0,0 +1,25 @@ +import { createWidget } from 'discourse/widgets/widget'; +import { h } from 'virtual-dom'; +import { dateNode } from 'discourse/helpers/node'; + +createWidget('large-notification-item', { + buildClasses(attrs) { + const result = ['item', 'notification']; + if (!attrs.get('read')) { + result.push('unread'); + } + return result; + }, + + html(attrs) { + return [this.attach('notification-item', attrs), + h('span.time', dateNode(attrs.created_at))]; + } +}); + +export default createWidget('user-notifications-large', { + html(attrs) { + const notifications = attrs.notifications; + return notifications.map(n => this.attach('large-notification-item', n)); + } +}); diff --git a/app/assets/javascripts/discourse/widgets/user-notifications.js.es6 b/app/assets/javascripts/discourse/widgets/user-notifications.js.es6 new file mode 100644 index 00000000000..8d3e710a0d1 --- /dev/null +++ b/app/assets/javascripts/discourse/widgets/user-notifications.js.es6 @@ -0,0 +1,81 @@ +import { createWidget } from 'discourse/widgets/widget'; +import { headerHeight } from 'discourse/components/site-header'; +import { h } from 'virtual-dom'; + +export default createWidget('user-notifications', { + tagName: 'div.notifications', + buildKey: () => 'user-notifications', + + defaultState() { + return { notifications: [], loading: false }; + }, + + notificationsChanged() { + this.refreshNotifications(this.state); + }, + + refreshNotifications(state) { + if (this.loading) { return; } + + // estimate (poorly) the amount of notifications to return + let limit = Math.round(($(window).height() - headerHeight()) / 55); + // we REALLY don't want to be asking for negative counts of notifications + // less than 5 is also not that useful + if (limit < 5) { limit = 5; } + if (limit > 40) { limit = 40; } + + const stale = this.store.findStale('notification', {recent: true, limit }, {cacheKey: 'recent-notifications'}); + + if (stale.hasResults) { + const results = stale.results; + let content = results.get('content'); + + // we have to truncate to limit, otherwise we will render too much + if (content && (content.length > limit)) { + content = content.splice(0, limit); + results.set('content', content); + results.set('totalRows', limit); + } + + state.notifications = results; + } else { + state.loading = true; + } + + stale.refresh().then(notifications => { + this.currentUser.set('unread_notifications', 0); + state.notifications = notifications; + }).catch(() => { + state.notifications = []; + }).finally(() => { + state.loading = false; + this.scheduleRerender(); + }); + }, + + html(attrs, state) { + if (!state.notifications.length) { + this.refreshNotifications(state); + } + + const result = []; + if (state.loading) { + result.push(h('div.spinner-container', h('div.spinner'))); + } else if (state.notifications.length) { + + const notificationItems = state.notifications.map(n => this.attach('notification-item', n)); + const href = `${attrs.path}/notifications`; + + result.push(h('hr')); + result.push(h('ul', [ + notificationItems, + h('li.read.last.heading', + h('a', { attributes: { href } }, [I18n.t('notifications.more'), '...']) + ) + ])); + + } + + return result; + } +}); diff --git a/app/assets/javascripts/discourse/widgets/widget.js.es6 b/app/assets/javascripts/discourse/widgets/widget.js.es6 index 7bbc3c10ec3..09c2deac3f5 100644 --- a/app/assets/javascripts/discourse/widgets/widget.js.es6 +++ b/app/assets/javascripts/discourse/widgets/widget.js.es6 @@ -1,4 +1,4 @@ -import { WidgetClickHook, WidgetClickOutsideHook } from 'discourse/widgets/click-hook'; +import { WidgetClickHook, WidgetClickOutsideHook, WidgetKeyUpHook } from 'discourse/widgets/hooks'; import { h } from 'virtual-dom'; import DecoratorHelper from 'discourse/widgets/decorator-helper'; @@ -66,6 +66,11 @@ function drawWidget(builder, attrs, state) { if (this.buildAttributes) { properties.attributes = this.buildAttributes(attrs); } + + if (this.keyUp) { + properties['widget-key-up'] = new WidgetKeyUpHook(this); + } + if (this.clickOutside) { properties['widget-click-outside'] = new WidgetClickOutsideHook(this); } @@ -119,9 +124,17 @@ export default class Widget { this.key = this.buildKey ? this.buildKey(attrs) : null; + // Helps debug widgets + if (Ember.Test) { + if (Object.keys(this.defaultState(attrs)).length > 0 && !this.key) { + Ember.warn(`you need a key when using state ${this.name}`); + } + } + this.site = container.lookup('site:main'); this.siteSettings = container.lookup('site-settings:main'); this.currentUser = container.lookup('current-user:main'); + this.capabilities = container.lookup('capabilities:main'); this.store = container.lookup('store:main'); this.appEvents = container.lookup('app-events:main'); this.keyValueStore = container.lookup('key-value-store:main'); @@ -143,7 +156,7 @@ export default class Widget { } render(prev) { - if (prev && prev.state) { + if (prev && prev.key && prev.key === this.key) { this.state = prev.state; } else { this.state = this.defaultState(this.attrs, this.state); @@ -166,7 +179,7 @@ export default class Widget { const refreshAction = dirtyOpts.onRefresh; if (refreshAction) { - this.sendWidgetAction(refreshAction); + this.sendWidgetAction(refreshAction, dirtyOpts.refreshArg); } } @@ -243,7 +256,7 @@ export default class Widget { if (target) { // TODO: Use ember closure actions - const actions = target._actions || target.actionHooks; + const actions = target._actions || target.actionHooks || {}; const method = actions[actionName]; if (method) { promise = method.call(target, param); @@ -276,6 +289,16 @@ export default class Widget { return result; } + sendWidgetEvent(name) { + const methodName = `${name}Event`; + return this.rerenderResult(() => { + const widget = this._findAncestorWithProperty(methodName); + if (widget) { + return widget[methodName](); + } + }); + } + sendWidgetAction(name, param) { return this.rerenderResult(() => { const widget = this._findAncestorWithProperty(name); diff --git a/app/assets/javascripts/main_include.js b/app/assets/javascripts/main_include.js index 0c11c439890..0af76b9aaf0 100644 --- a/app/assets/javascripts/main_include.js +++ b/app/assets/javascripts/main_include.js @@ -63,12 +63,11 @@ //= require ./discourse/components/combo-box //= require ./discourse/components/edit-category-panel //= require ./discourse/views/button -//= require ./discourse/components/search-result //= require ./discourse/components/dropdown-button //= require ./discourse/components/notifications-button //= require ./discourse/components/topic-notifications-button //= require ./discourse/lib/link-mentions -//= require ./discourse/views/header +//= require ./discourse/components/site-header //= require ./discourse/lib/utilities //= require ./discourse/dialects/dialect //= require ./discourse/lib/emoji/emoji diff --git a/app/assets/stylesheets/common/base/header.scss b/app/assets/stylesheets/common/base/header.scss index 610832b1c83..24e28ba2ba2 100644 --- a/app/assets/stylesheets/common/base/header.scss +++ b/app/assets/stylesheets/common/base/header.scss @@ -131,7 +131,7 @@ right: 65px; } } - .flagged-posts { + .flagged-posts, .queued-posts { background: $danger; } } diff --git a/test/javascripts/acceptance/hamburger-menu-staff-test.js.es6 b/test/javascripts/acceptance/hamburger-menu-staff-test.js.es6 deleted file mode 100644 index d4bdd05f6f0..00000000000 --- a/test/javascripts/acceptance/hamburger-menu-staff-test.js.es6 +++ /dev/null @@ -1,13 +0,0 @@ -import { acceptance } from "helpers/qunit-helpers"; - -acceptance("Hamburger Menu - Staff", { loggedIn: true }); - -test("Menu Items", (assert) => { - visit("/"); - click("#toggle-hamburger-menu"); - andThen(() => { - assert.ok(exists(".hamburger-panel .admin-link")); - assert.ok(exists(".hamburger-panel .flagged-posts-link")); - assert.ok(exists(".hamburger-panel .flagged-posts.badge-notification"), "it displays flag notifications"); - }); -}); diff --git a/test/javascripts/acceptance/hamburger-menu-test.js.es6 b/test/javascripts/acceptance/hamburger-menu-test.js.es6 deleted file mode 100644 index 421acf9165a..00000000000 --- a/test/javascripts/acceptance/hamburger-menu-test.js.es6 +++ /dev/null @@ -1,21 +0,0 @@ -import { acceptance } from "helpers/qunit-helpers"; - -acceptance("Hamburger Menu"); - -test("Menu Items", (assert) => { - visit("/"); - click("#toggle-hamburger-menu"); - andThen(() => { - assert.ok(!exists(".hamburger-panel .admin-link"), 'does not have admin link'); - assert.ok(!exists(".hamburger-panel .flagged-posts-link"), 'does not have flagged posts link'); - - assert.ok(exists(".hamburger-panel .latest-topics-link"), 'last link to latest'); - assert.ok(exists(".hamburger-panel .badge-link"), 'has link to badges'); - assert.ok(exists(".hamburger-panel .user-directory-link"), 'has user directory link'); - assert.ok(exists(".hamburger-panel .faq-link"), 'has faq link'); - assert.ok(exists(".hamburger-panel .about-link"), 'has about link'); - assert.ok(exists(".hamburger-panel .categories-link"), 'has categories link'); - - assert.ok(exists('.hamburger-panel .category-link'), 'has at least one category'); - }); -}); diff --git a/test/javascripts/acceptance/header-anonymous-test.js.es6 b/test/javascripts/acceptance/header-anonymous-test.js.es6 deleted file mode 100644 index 088cb4a30f5..00000000000 --- a/test/javascripts/acceptance/header-anonymous-test.js.es6 +++ /dev/null @@ -1,30 +0,0 @@ -import { acceptance } from "helpers/qunit-helpers"; -acceptance("Header (Anonymous)"); - -test("header", () => { - visit("/"); - andThen(() => { - ok(exists("header"), "is rendered"); - ok(exists(".logo-big"), "it renders the large logo by default"); - not(exists("#notifications-dropdown li"), "no notifications at first"); - not(exists("#user-dropdown:visible"), "initially user dropdown is closed"); - not(exists("#search-dropdown:visible"), "initially search box is closed"); - }); - - // Logo changing - andThen(() => { - controllerFor('header').set("showExtraInfo", true); - }); - - andThen(() => { - ok(exists(".logo-small"), "it shows the small logo when `showExtraInfo` is enabled"); - }); - - // Search - click("#search-button"); - andThen(() => { - 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"); - }); - -}); diff --git a/test/javascripts/acceptance/header-test-staff.js.es6 b/test/javascripts/acceptance/header-test-staff.js.es6 deleted file mode 100644 index 9312db02122..00000000000 --- a/test/javascripts/acceptance/header-test-staff.js.es6 +++ /dev/null @@ -1,14 +0,0 @@ -import { acceptance } from "helpers/qunit-helpers"; - -acceptance("Header (Staff)", { loggedIn: true }); - -test("header", () => { - visit("/"); - - // User dropdown - click("#current-user"); - andThen(() => { - ok(exists(".user-menu:visible"), "is lazily rendered after user opens it"); - ok(exists(".user-menu .menu-links-header"), "has showing / hiding user-dropdown links correctly bound"); - }); -}); diff --git a/test/javascripts/acceptance/search-test.js.es6 b/test/javascripts/acceptance/search-test.js.es6 index 71208a74640..7b0c8dc9b76 100644 --- a/test/javascripts/acceptance/search-test.js.es6 +++ b/test/javascripts/acceptance/search-test.js.es6 @@ -12,6 +12,7 @@ test("search", (assert) => { }); fillIn('#search-term', 'dev'); + keyEvent('#search-term', 'keyup', 16); andThen(() => { assert.ok(exists('.search-menu .results ul li'), 'it shows results'); }); diff --git a/test/javascripts/components/menu-panel-test.js.es6 b/test/javascripts/components/menu-panel-test.js.es6 deleted file mode 100644 index 6c586eed7c4..00000000000 --- a/test/javascripts/components/menu-panel-test.js.es6 +++ /dev/null @@ -1,65 +0,0 @@ -import componentTest from 'helpers/component-test'; -moduleForComponent('menu-panel', {integration: true}); - -componentTest('as a dropdown', { - template: ` -
    click me
    - - - - {{#menu-panel visible=panelVisible markActive=".menu-selected" force="drop-down"}} - Some content - {{/menu-panel}} - `, - - setup() { - this.set('panelVisible', false); - }, - - test(assert) { - assert.ok(exists(".menu-panel.hidden"), "hidden by default"); - - this.set('panelVisible', true); - andThen(() => { - assert.ok(!exists(".menu-panel.hidden"), "toggling visible makes it appear"); - }); - - click('#outside-area'); - andThen(() => { - assert.ok(exists(".menu-panel.hidden"), "clicking the body hides the menu"); - assert.equal(this.get('panelVisible'), false, 'it updates the bound variable'); - }); - } -}); - -componentTest('as a slide-in', { - template: ` -
    click me
    - - - {{#menu-panel visible=panelVisible markActive=".menu-selected" force="slide-in"}} - Some content - {{/menu-panel}} - `, - - setup() { - this.set('panelVisible', false); - }, - - test(assert) { - assert.ok(exists(".menu-panel.hidden"), "hidden by default"); - - this.set('panelVisible', true); - andThen(() => { - assert.ok(!exists(".menu-panel.hidden"), "toggling visible makes it appear"); - }); - - click('#outside-area'); - andThen(() => { - assert.ok(exists(".menu-panel.hidden"), "clicking the body hides the menu"); - assert.equal(this.get('panelVisible'), false, 'it updates the bound variable'); - this.set('panelVisible', true); - }); - - } -}); diff --git a/test/javascripts/controllers/topic-test.js.es6 b/test/javascripts/controllers/topic-test.js.es6 index 48f0bcdbd02..90f4dda169b 100644 --- a/test/javascripts/controllers/topic-test.js.es6 +++ b/test/javascripts/controllers/topic-test.js.es6 @@ -1,7 +1,7 @@ import { blank, present } from 'helpers/qunit-helpers'; moduleFor('controller:topic', 'controller:topic', { - needs: ['controller:header', 'controller:modal', 'controller:composer', 'controller:quote-button', + needs: ['controller:modal', 'controller:composer', 'controller:quote-button', 'controller:topic-progress', 'controller:application'] }); diff --git a/test/javascripts/helpers/component-test.js.es6 b/test/javascripts/helpers/component-test.js.es6 index 995fb476218..7e8fc08eeb7 100644 --- a/test/javascripts/helpers/component-test.js.es6 +++ b/test/javascripts/helpers/component-test.js.es6 @@ -1,6 +1,7 @@ import AppEvents from 'discourse/lib/app-events'; import createStore from 'helpers/create-store'; import { autoLoadModules } from 'discourse/initializers/auto-load-modules'; +import TopicTrackingState from 'discourse/models/topic-tracking-state'; export default function(name, opts) { opts = opts || {}; @@ -22,11 +23,19 @@ export default function(name, opts) { autoLoadModules(); - if (opts.setup) { - const store = createStore(); - this.currentUser = Discourse.User.create(); - this.container.register('store:main', store, { instantiate: false }); + const store = createStore(); + if (!opts.anonymous) { + const currentUser = Discourse.User.create({ username: 'eviltrout' }); + this.currentUser = currentUser; this.container.register('current-user:main', this.currentUser, { instantiate: false }); + this.container.register('topic-tracking-state:main', + TopicTrackingState.create({ currentUser }), + { instantiate: false }); + } + + this.container.register('store:main', store, { instantiate: false }); + + if (opts.setup) { opts.setup.call(this, store); } diff --git a/test/javascripts/helpers/qunit-helpers.js.es6 b/test/javascripts/helpers/qunit-helpers.js.es6 index c6905856612..63288ef7abd 100644 --- a/test/javascripts/helpers/qunit-helpers.js.es6 +++ b/test/javascripts/helpers/qunit-helpers.js.es6 @@ -2,7 +2,7 @@ import sessionFixtures from 'fixtures/session-fixtures'; import siteFixtures from 'fixtures/site-fixtures'; -import HeaderView from 'discourse/views/header'; +import HeaderComponent from 'discourse/components/site-header'; function currentUser() { return Discourse.User.create(sessionFixtures['/session/current.json'].current_user); @@ -41,7 +41,7 @@ function acceptance(name, options) { Discourse.Utilities.avatarImg = () => ""; // For now don't do scrolling stuff in Test Mode - HeaderView.reopen({examineDockHeader: Ember.K}); + HeaderComponent.reopen({examineDockHeader: Ember.K}); var siteJson = siteFixtures['site.json'].site; if (options) { diff --git a/test/javascripts/widgets/actions-summary-test.js.es6 b/test/javascripts/widgets/actions-summary-test.js.es6 index 8d549ff4992..9b7f9cea233 100644 --- a/test/javascripts/widgets/actions-summary-test.js.es6 +++ b/test/javascripts/widgets/actions-summary-test.js.es6 @@ -7,8 +7,8 @@ widgetTest('listing actions', { setup() { this.set('args', { actionsSummary: [ - {action: 'off_topic', description: 'very off topic'}, - {action: 'spam', description: 'suspicious message'} + {id: 1, action: 'off_topic', description: 'very off topic'}, + {id: 2, action: 'spam', description: 'suspicious message'} ] }); }, diff --git a/test/javascripts/widgets/hamburger-menu-test.js.es6 b/test/javascripts/widgets/hamburger-menu-test.js.es6 new file mode 100644 index 00000000000..a6ed500761d --- /dev/null +++ b/test/javascripts/widgets/hamburger-menu-test.js.es6 @@ -0,0 +1,173 @@ +import { moduleForWidget, widgetTest } from 'helpers/widget-test'; + +moduleForWidget('hamburger-menu'); + +widgetTest('prioritize faq', { + template: '{{mount-widget widget="hamburger-menu"}}', + + setup() { + this.siteSettings.faq_url = 'http://example.com/faq'; + this.currentUser.set('read_faq', false); + }, + + test(assert) { + assert.ok(this.$('.faq-priority').length); + assert.ok(!this.$('.faq-link').length); + } +}); + +widgetTest('prioritize faq - user has read', { + template: '{{mount-widget widget="hamburger-menu"}}', + + setup() { + this.siteSettings.faq_url = 'http://example.com/faq'; + this.currentUser.set('read_faq', true); + }, + + test(assert) { + assert.ok(!this.$('.faq-priority').length); + assert.ok(this.$('.faq-link').length); + } +}); + +widgetTest('staff menu - not staff', { + template: '{{mount-widget widget="hamburger-menu"}}', + + setup() { + this.currentUser.set('staff', false); + }, + + test(assert) { + assert.ok(!this.$('.admin-link').length); + } +}); + +widgetTest('staff menu', { + template: '{{mount-widget widget="hamburger-menu"}}', + + setup() { + this.currentUser.setProperties({ staff: true, site_flagged_posts_count: 3 }); + }, + + test(assert) { + assert.ok(this.$('.admin-link').length); + assert.ok(this.$('.flagged-posts-link').length); + assert.equal(this.$('.flagged-posts').text(), '3'); + assert.ok(!this.$('.settings-link').length); + } +}); + +widgetTest('staff menu - admin', { + template: '{{mount-widget widget="hamburger-menu"}}', + + setup() { + this.currentUser.setProperties({ staff: true, admin: true }); + }, + + test(assert) { + assert.ok(this.$('.settings-link').length); + } +}); + + +widgetTest('queued posts', { + template: '{{mount-widget widget="hamburger-menu"}}', + + setup() { + this.currentUser.setProperties({ + staff: true, + show_queued_posts: true, + post_queue_new_count: 5 + }); + }, + + test(assert) { + assert.ok(this.$('.queued-posts-link').length); + assert.equal(this.$('.queued-posts').text(), '5'); + } +}); + +widgetTest('queued posts - disabled', { + template: '{{mount-widget widget="hamburger-menu"}}', + + setup() { + this.currentUser.setProperties({ staff: true, show_queued_posts: false }); + }, + + test(assert) { + assert.ok(!this.$('.queued-posts-link').length); + } +}); + + +widgetTest('logged in links', { + template: '{{mount-widget widget="hamburger-menu"}}', + + test(assert) { + assert.ok(this.$('.new-topics-link').length); + assert.ok(this.$('.unread-topics-link').length); + } +}); + +widgetTest('general links', { + template: '{{mount-widget widget="hamburger-menu"}}', + anonymous: true, + + test(assert) { + assert.ok(this.$('.latest-topics-link').length); + assert.ok(!this.$('.new-topics-link').length); + assert.ok(!this.$('.unread-topics-link').length); + assert.ok(this.$('.top-topics-link').length); + assert.ok(this.$('.badge-link').length); + assert.ok(this.$('.category-link').length > 0); + } +}); + +widgetTest('badges link - disabled', { + template: '{{mount-widget widget="hamburger-menu"}}', + + setup() { + this.siteSettings.enable_badges = false; + }, + + test(assert) { + assert.ok(!this.$('.badge-link').length); + } +}); + +widgetTest('badges link', { + template: '{{mount-widget widget="hamburger-menu"}}', + + test(assert) { + assert.ok(this.$('.badge-link').length); + } +}); + +widgetTest('user directory link', { + template: '{{mount-widget widget="hamburger-menu"}}', + + test(assert) { + assert.ok(this.$('.user-directory-link').length); + } +}); + +widgetTest('user directory link - disabled', { + template: '{{mount-widget widget="hamburger-menu"}}', + + setup() { + this.siteSettings.enable_user_directory = false; + }, + + test(assert) { + assert.ok(!this.$('.user-directory-link').length); + } +}); + +widgetTest('general links', { + template: '{{mount-widget widget="hamburger-menu"}}', + + test(assert) { + assert.ok(this.$('.about-link').length); + assert.ok(this.$('.keyboard-shortcuts-link').length); + } +}); diff --git a/test/javascripts/widgets/header-test.js.es6 b/test/javascripts/widgets/header-test.js.es6 new file mode 100644 index 00000000000..f3a7b8f9071 --- /dev/null +++ b/test/javascripts/widgets/header-test.js.es6 @@ -0,0 +1,37 @@ +import { moduleForWidget, widgetTest } from 'helpers/widget-test'; + +moduleForWidget('header'); + +widgetTest('rendering basics', { + template: '{{mount-widget widget="header"}}', + test(assert) { + assert.ok(this.$('header.d-header').length); + assert.ok(this.$('#site-logo').length); + } +}); + +widgetTest('sign up / login buttons', { + template: '{{mount-widget widget="header" showCreateAccount="showCreateAccount" showLogin="showLogin" args=args}}', + anonymous: true, + + setup() { + this.set('args', { canSignUp: true }); + this.on('showCreateAccount', () => this.signupShown = true); + this.on('showLogin', () => this.loginShown = true); + }, + + test(assert) { + assert.ok(this.$('button.sign-up-button').length); + assert.ok(this.$('button.login-button').length); + + click('button.sign-up-button'); + andThen(() => { + assert.ok(this.signupShown); + }); + + click('button.login-button'); + andThen(() => { + assert.ok(this.loginShown); + }); + } +}); diff --git a/test/javascripts/components/home-logo-test.js.es6 b/test/javascripts/widgets/home-logo-test.js.es6 similarity index 53% rename from test/javascripts/components/home-logo-test.js.es6 rename to test/javascripts/widgets/home-logo-test.js.es6 index 60eefd94b9f..9c4e6780588 100644 --- a/test/javascripts/components/home-logo-test.js.es6 +++ b/test/javascripts/widgets/home-logo-test.js.es6 @@ -1,19 +1,19 @@ -import componentTest from 'helpers/component-test'; +import { moduleForWidget, widgetTest } from 'helpers/widget-test'; -moduleForComponent('home-logo', {integration: true}); +moduleForWidget('home-logo'); const bigLogo = '/images/d-logo-sketch.png?test'; const smallLogo = '/images/d-logo-sketch-small.png?test'; const mobileLogo = '/images/d-logo-sketch.png?mobile'; const title = "Cool Forum"; -componentTest('basics', { - template: '{{home-logo minimized=minimized}}', +widgetTest('basics', { + template: '{{mount-widget widget="home-logo" args=args}}', setup() { this.siteSettings.logo_url = bigLogo; this.siteSettings.logo_small_url= smallLogo; this.siteSettings.title = title; - this.set('minimized', false); + this.set('args', { minimized: false }); }, test(assert) { @@ -23,23 +23,32 @@ componentTest('basics', { assert.ok(this.$('img#site-logo.logo-big').length === 1); assert.equal(this.$('#site-logo').attr('src'), bigLogo); assert.equal(this.$('#site-logo').attr('alt'), title); - - this.set('minimized', true); - andThen(() => { - assert.ok(this.$('img.logo-small').length === 1); - assert.equal(this.$('img.logo-small').attr('src'), smallLogo); - assert.equal(this.$('img.logo-small').attr('alt'), title); - }); } }); -componentTest('no logo', { - template: '{{home-logo minimized=minimized}}', +widgetTest('basics - minmized', { + template: '{{mount-widget widget="home-logo" args=args}}', + setup() { + this.siteSettings.logo_url = bigLogo; + this.siteSettings.logo_small_url= smallLogo; + this.siteSettings.title = title; + this.set('args', { minimized: true }); + }, + + test(assert) { + assert.ok(this.$('img.logo-small').length === 1); + assert.equal(this.$('img.logo-small').attr('src'), smallLogo); + assert.equal(this.$('img.logo-small').attr('alt'), title); + } +}); + +widgetTest('no logo', { + template: '{{mount-widget widget="home-logo" args=args}}', setup() { this.siteSettings.logo_url = ''; this.siteSettings.logo_small_url = ''; this.siteSettings.title = title; - this.set('minimized', false); + this.set('args', { minimized: false }); }, test(assert) { @@ -47,16 +56,25 @@ componentTest('no logo', { assert.ok(this.$('h2#site-text-logo.text-logo').length === 1); assert.equal(this.$('#site-text-logo').text(), title); - - this.set('minimized', true); - andThen(() => { - assert.ok(this.$('i.fa-home').length === 1); - }); } }); -componentTest('mobile logo', { - template: "{{home-logo}}", +widgetTest('no logo - minimized', { + template: '{{mount-widget widget="home-logo" args=args}}', + setup() { + this.siteSettings.logo_url = ''; + this.siteSettings.logo_small_url = ''; + this.siteSettings.title = title; + this.set('args', { minimized: true }); + }, + + test(assert) { + assert.ok(this.$('i.fa-home').length === 1); + } +}); + +widgetTest('mobile logo', { + template: '{{mount-widget widget="home-logo" args=args}}', setup() { this.siteSettings.mobile_logo_url = mobileLogo; this.siteSettings.logo_small_url= smallLogo; @@ -69,8 +87,8 @@ componentTest('mobile logo', { } }); -componentTest('mobile without logo', { - template: "{{home-logo}}", +widgetTest('mobile without logo', { + template: '{{mount-widget widget="home-logo" args=args}}', setup() { this.siteSettings.logo_url = bigLogo; this.site.mobileView = true; @@ -81,10 +99,3 @@ componentTest('mobile without logo', { assert.equal(this.$('#site-logo').attr('src'), bigLogo); } }); - -componentTest("changing url", { - template: '{{home-logo targetUrl="https://www.discourse.org"}}', - test(assert) { - assert.equal(this.$('a').attr('href'), 'https://www.discourse.org'); - } -}); diff --git a/test/javascripts/widgets/post-gutter-test.js.es6 b/test/javascripts/widgets/post-gutter-test.js.es6 index 55c4152cb28..2de86d6761d 100644 --- a/test/javascripts/widgets/post-gutter-test.js.es6 +++ b/test/javascripts/widgets/post-gutter-test.js.es6 @@ -6,6 +6,7 @@ widgetTest("duplicate links", { template: '{{mount-widget widget="post-gutter" args=args}}', setup() { this.set('args', { + id: 2, links: [ { title: "Evil Trout Link", url: "http://eviltrout.com" }, { title: "Evil Trout Link", url: "http://dupe.eviltrout.com" } @@ -21,6 +22,7 @@ widgetTest("collapsed links", { template: '{{mount-widget widget="post-gutter" args=args}}', setup() { this.set('args', { + id: 1, links: [ { title: "Link 1", url: "http://eviltrout.com?1" }, { title: "Link 2", url: "http://eviltrout.com?2" }, diff --git a/test/javascripts/widgets/user-menu-test.js.es6 b/test/javascripts/widgets/user-menu-test.js.es6 new file mode 100644 index 00000000000..3c043090eb1 --- /dev/null +++ b/test/javascripts/widgets/user-menu-test.js.es6 @@ -0,0 +1,106 @@ +import { moduleForWidget, widgetTest } from 'helpers/widget-test'; + +moduleForWidget('user-menu'); + +widgetTest('basics', { + template: '{{mount-widget widget="user-menu"}}', + + test(assert) { + assert.ok(this.$('.user-menu').length); + assert.ok(this.$('.user-activity-link').length); + assert.ok(this.$('.user-bookmarks-link').length); + assert.ok(this.$('.user-preferences-link').length); + assert.ok(this.$('.notifications').length); + } +}); + +widgetTest('log out', { + template: '{{mount-widget widget="user-menu" logout="logout"}}', + + setup() { + this.on('logout', () => this.loggedOut = true); + }, + + test(assert) { + assert.ok(this.$('.logout').length); + + click('.logout'); + andThen(() => { + assert.ok(this.loggedOut); + }); + } +}); + +widgetTest('private messages - disabled', { + template: '{{mount-widget widget="user-menu"}}', + setup() { + this.siteSettings.enable_private_messages = false; + }, + + test(assert) { + assert.ok(!this.$('.user-pms-link').length); + } +}); + +widgetTest('private messages - enabled', { + template: '{{mount-widget widget="user-menu"}}', + setup() { + this.siteSettings.enable_private_messages = true; + }, + + test(assert) { + assert.ok(this.$('.user-pms-link').length); + } +}); + +widgetTest('anonymous', { + template: '{{mount-widget widget="user-menu" toggleAnonymous="toggleAnonymous"}}', + + setup() { + this.currentUser.setProperties({ is_anonymous: false, trust_level: 3 }); + this.siteSettings.allow_anonymous_posting = true; + this.siteSettings.anonymous_posting_min_trust_level = 3; + + this.on('toggleAnonymous', () => this.anonymous = true); + }, + + test(assert) { + assert.ok(this.$('.enable-anonymous').length); + click('.enable-anonymous'); + andThen(() => { + assert.ok(this.anonymous); + }); + } +}); + +widgetTest('anonymous - disabled', { + template: '{{mount-widget widget="user-menu"}}', + + setup() { + this.siteSettings.allow_anonymous_posting = false; + }, + + test(assert) { + assert.ok(!this.$('.enable-anonymous').length); + } +}); + +widgetTest('anonymous - switch back', { + template: '{{mount-widget widget="user-menu" toggleAnonymous="toggleAnonymous"}}', + + setup() { + this.currentUser.setProperties({ is_anonymous: true }); + this.siteSettings.allow_anonymous_posting = true; + + this.on('toggleAnonymous', () => this.anonymous = true); + }, + + test(assert) { + assert.ok(this.$('.disable-anonymous').length); + click('.disable-anonymous'); + andThen(() => { + assert.ok(this.anonymous); + }); + } +}); + diff --git a/test/javascripts/widgets/widget-test.js.es6 b/test/javascripts/widgets/widget-test.js.es6 index 5680516f6bf..ed52f81bee4 100644 --- a/test/javascripts/widgets/widget-test.js.es6 +++ b/test/javascripts/widgets/widget-test.js.es6 @@ -89,6 +89,7 @@ widgetTest('widget state', { setup() { createWidget('state-test', { tagName: 'button.test', + buildKey: () => `button-test`, defaultState() { return { clicks: 0 }; @@ -121,6 +122,7 @@ widgetTest('widget update with promise', { setup() { createWidget('promise-test', { tagName: 'button.test', + buildKey: () => 'promise-test', html(attrs, state) { return state.name || "No name";