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(`${title}
`);
- }
- 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 @@
-
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}}
-
-{{/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}}
-
-
- {{/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}}
-
- {{#if currentUser}}
- {{plugin-outlet "header-before-notifications"}}
- {{/if}}
-
- {{#header-dropdown iconId="search-button"
- icon="search"
- action="toggleSearch"
- toggleVisible=searchVisible
- mobileAction="fullPageSearch"
- loginAction="showLogin"
- title="search.title"
- path="/search"}}
- {{/header-dropdown}}
-
- {{#header-dropdown iconId="toggle-hamburger-menu"
- icon="bars"
- toggleVisible=hamburgerVisible
- loginAction="showLogin"
- title="hamburger_menu"}}
- {{#if flaggedPostsCount}}
- {{flaggedPostsCount}}
- {{/if}}
- {{/header-dropdown}}
-
- {{#if currentUser}}
- {{#header-dropdown iconId="current-user"
- class="current-user"
- showUser="true"
- toggleVisible=userMenuVisible
- loginAction="showLogin"
- title="user.avatar.header_title"}}
- {{#if currentUser.unread_notifications}}
- {{currentUser.unread_notifications}}
- {{/if}}
- {{#if currentUser.unread_private_messages}}
- {{currentUser.unread_private_messages}}
- {{/if}}
- {{plugin-outlet "header-notifications"}}
- {{/header-dropdown}}
- {{/if}}
-
- {{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";