PERF: Migrate header to discourse widgets

This commit is contained in:
Robin Ward 2016-04-14 15:23:05 -04:00
parent d1f61015c0
commit 514c3976f0
No known key found for this signature in database
GPG Key ID: 0E091E2B4ED1B83D
91 changed files with 2179 additions and 1640 deletions

View File

@ -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();
}
}
});

View File

@ -1,14 +1,7 @@
import computed from 'ember-addons/ember-computed-decorators';
export default Ember.Component.extend({ export default Ember.Component.extend({
tagName: 'li', tagName: 'li',
classNameBindings: [':header-dropdown-toggle', 'active'], 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'), active: Ember.computed.alias('toggleVisible'),
actions: { actions: {

View File

@ -1,54 +1,3 @@
import DiscourseURL from 'discourse/lib/url'; export function needsSecondRowIf() {
Ember.warn("DEPRECATION: `needsSecondRowIf` is deprecated. Use widget hooks on `header-second-row`");
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);
} }
needsSecondRowIf('topic.category', function(cat) {
return cat && (!cat.get('isUncategorizedCategory') || !this.siteSettings.suppress_uncategorized_badge);
});
export default TopicCategoryComponent;
export { needsSecondRowIf };

View File

@ -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(`<a href="${this.linkUrl}" data-auto-route="true">`);
if (!this.site.mobileView && this.get('minimized')) {
const logoSmallUrl = siteSettings.logo_small_url || '';
if (logoSmallUrl.length) {
buffer.push(`<img id='site-logo' class="logo-small" src="${logoSmallUrl}" width="33" height="33" alt="${title}">`);
} else {
buffer.push(iconHTML('home'));
}
} else if (this.showMobileLogo) {
buffer.push(`<img id="site-logo" class="logo-big" src="${siteSettings.mobile_logo_url}" alt="${title}">`);
} else if (logoUrl.length) {
buffer.push(`<img id="site-logo" class="logo-big" src="${logoUrl}" alt="${title}">`);
} else {
buffer.push(`<h2 id="site-text-logo" class="text-logo">${title}</h2>`);
}
buffer.push('</a>');
}
});

View File

@ -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();
}
}
});

View File

@ -1,5 +1,6 @@
import { keyDirty } from 'discourse/widgets/widget';
import { diff, patch } from 'virtual-dom'; 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'; import { renderedKey, queryRegistry } from 'discourse/widgets/widget';
const _cleanCallbacks = {}; const _cleanCallbacks = {};
@ -13,13 +14,20 @@ export default Ember.Component.extend({
_rootNode: null, _rootNode: null,
_timeout: null, _timeout: null,
_widgetClass: null, _widgetClass: null,
_afterRender: null, _renderCallback: null,
_childEvents: null,
init() { init() {
this._super(); this._super();
const name = this.get('widget'); const name = this.get('widget');
this._widgetClass = queryRegistry(name) || this.container.lookupFactory(`widget:${name}`); this._widgetClass = queryRegistry(name) || this.container.lookupFactory(`widget:${name}`);
if (!this._widgetClass) {
console.error(`Error: Could not find widget: ${name}`);
}
this._childEvents = [];
this._connected = []; this._connected = [];
}, },
@ -42,50 +50,64 @@ export default Ember.Component.extend({
}, },
willDestroyElement() { willDestroyElement() {
this._childEvents.forEach(evt => this.appEvents.off(evt));
Ember.run.cancel(this._timeout); 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) { queueRerender(callback) {
if (callback && !this._afterRender) { if (callback && !this._renderCallback) {
this._afterRender = callback; this._renderCallback = callback;
} }
Ember.run.scheduleOnce('render', this, this.rerenderWidget); Ember.run.scheduleOnce('render', this, this.rerenderWidget);
}, },
buildArgs() {
},
rerenderWidget() { rerenderWidget() {
Ember.run.cancel(this._timeout); Ember.run.cancel(this._timeout);
if (this._rootNode) { if (this._rootNode) {
if (!this._widgetClass) { return; }
const t0 = new Date().getTime(); const t0 = new Date().getTime();
const args = this.get('args') || this.buildArgs();
const opts = { model: this.get('model') }; 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; newTree._emberView = this;
const patches = diff(this._tree || this._rootNode, newTree); const patches = diff(this._tree || this._rootNode, newTree);
const $body = $(document); this.beforePatch();
const prevHeight = $body.height();
const prevScrollTop = $body.scrollTop();
this._rootNode = patch(this._rootNode, patches); this._rootNode = patch(this._rootNode, patches);
this.afterPatch();
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._tree = newTree; this._tree = newTree;
if (this._afterRender) { if (this._renderCallback) {
this._afterRender(); this._renderCallback();
this._afterRender = null; this._renderCallback = null;
} }
this.afterRender();
renderedKey('*'); renderedKey('*');
if (this.profileWidget) { if (this.profileWidget) {

View File

@ -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('<a href="' + url + '" alt="' + I18n.t('notifications.alt.' + this.get("name")) + '">' + text + '</a>');
} else {
buffer.push(text);
}
}
});

View File

@ -37,6 +37,25 @@ export default MountWidget.extend({
'searchService'); 'searchService');
}).volatile(), }).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() { scrolled() {
if (this.isDestroyed || this.isDestroying) { return; } if (this.isDestroyed || this.isDestroying) { return; }
if (isWorkaroundActive()) { return; } if (isWorkaroundActive()) { return; }

View File

@ -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');
}
}
});

View File

@ -1,2 +0,0 @@
import SearchResult from 'discourse/components/search-result';
export default SearchResult.extend();

View File

@ -1,2 +0,0 @@
import SearchResult from 'discourse/components/search-result';
export default SearchResult.extend();

View File

@ -1,2 +0,0 @@
import SearchResult from 'discourse/components/search-result';
export default SearchResult.extend();

View File

@ -1,2 +0,0 @@
import SearchResult from 'discourse/components/search-result';
export default SearchResult.extend();

View File

@ -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')
});

View File

@ -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());
}

View File

@ -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');
}
}
});

View File

@ -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();
}
});

View File

@ -386,7 +386,7 @@ export default Ember.Controller.extend({
let message = this.get('similarTopicsMessage'); let message = this.get('similarTopicsMessage');
if (!message) { if (!message) {
message = Discourse.ComposerMessage.create({ message = Discourse.ComposerMessage.create({
templateName: 'composer/similar_topics', templateName: 'composer/similar-topics',
extraClass: 'similar-topics' extraClass: 'similar-topics'
}); });
this.set('similarTopicsMessage', message); this.set('similarTopicsMessage', message);

View File

@ -1,77 +1,6 @@
import DiscourseURL from 'discourse/lib/url'; import { addFlagProperty as realAddFlagProperty } from 'discourse/components/site-header';
const HeaderController = Ember.Controller.extend({ export function addFlagProperty(prop) {
topic: null, Ember.warn("importing `addFlagProperty` is deprecated. Use the PluginAPI instead");
showExtraInfo: null, realAddFlagProperty(prop);
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);
} }
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;

View File

@ -9,7 +9,7 @@ import Composer from 'discourse/models/composer';
import DiscourseURL from 'discourse/lib/url'; import DiscourseURL from 'discourse/lib/url';
export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { 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, multiSelect: false,
allPostsSelected: false, allPostsSelected: false,
editingTopic: false, editingTopic: false,
@ -472,11 +472,6 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
this.get('content').toggleStatus('archived'); this.get('content').toggleStatus('archived');
}, },
// Toggle the star on the topic
toggleStar() {
this.get('content').toggleStar();
},
clearPin() { clearPin() {
this.get('content').clearPin(); this.get('content').clearPin();
}, },
@ -625,10 +620,6 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
return false; return false;
}, },
showStarButton: function() {
return Discourse.User.current() && !this.get('model.isPrivateMessage');
}.property('model.isPrivateMessage'),
loadingHTML: function() { loadingHTML: function() {
return spinnerHTML; return spinnerHTML;
}.property(), }.property(),

View File

@ -10,13 +10,13 @@ export default Ember.ArrayController.extend({
currentPath: Em.computed.alias('controllers.application.currentPath'), currentPath: Em.computed.alias('controllers.application.currentPath'),
actions: { actions: {
resetNew: function() { resetNew() {
Discourse.ajax('/notifications/mark-read', { method: 'PUT' }).then(() => { Discourse.ajax('/notifications/mark-read', { method: 'PUT' }).then(() => {
this.setEach('read', true); this.setEach('read', true);
}); });
}, },
loadMore: function() { loadMore() {
this.get('model').loadMore(); this.get('model').loadMore();
} }
} }

View File

@ -1,4 +1,4 @@
import { applyFlaggedProperties } from 'discourse/controllers/header'; import { applyFlaggedProperties } from 'discourse/components/site-header';
export default { export default {
name: 'apply-flagged-properties', name: 'apply-flagged-properties',

View File

@ -10,7 +10,8 @@ export default {
siteSettings = container.lookup('site-settings:main'), siteSettings = container.lookup('site-settings:main'),
bus = container.lookup('message-bus:main'), bus = container.lookup('message-bus:main'),
keyValueStore = container.lookup('key-value-store: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 // clear old cached notifications, we used to store in local storage
// TODO 2017 delete this line // 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 oldUnread = user.get('unread_notifications');
const oldPM = user.get('unread_private_messages'); const oldPM = user.get('unread_private_messages');
@ -38,7 +39,7 @@ export default {
user.set('unread_private_messages', data.unread_private_messages); user.set('unread_private_messages', data.unread_private_messages);
if (oldUnread !== data.unread_notifications || oldPM !== 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'}); const stale = store.findStale('notification', {}, {cacheKey: 'recent-notifications'});

View File

@ -18,6 +18,7 @@ export default function interceptClick(e) {
$currentTarget.data('auto-route') || $currentTarget.data('auto-route') ||
$currentTarget.data('share-url') || $currentTarget.data('share-url') ||
$currentTarget.data('user-card') || $currentTarget.data('user-card') ||
$currentTarget.hasClass('widget-link') ||
$currentTarget.hasClass('mention') || $currentTarget.hasClass('mention') ||
(!$currentTarget.hasClass('d-link') && $currentTarget.hasClass('ember-view')) || (!$currentTarget.hasClass('d-link') && $currentTarget.hasClass('ember-view')) ||
$currentTarget.hasClass('lightbox') || $currentTarget.hasClass('lightbox') ||

View File

@ -10,8 +10,8 @@ const bindings = {
'.': {click: '.alert.alert-info.clickable', anonymous: true}, // show incoming/updated topics '.': {click: '.alert.alert-info.clickable', anonymous: true}, // show incoming/updated topics
'b': {handler: 'toggleBookmark'}, 'b': {handler: 'toggleBookmark'},
'c': {handler: 'createTopic'}, 'c': {handler: 'createTopic'},
'ctrl+f': {handler: 'showBuiltinSearch', anonymous: true}, 'ctrl+f': {handler: 'showPageSearch', anonymous: true},
'command+f': {handler: 'showBuiltinSearch', anonymous: true}, 'command+f': {handler: 'showPageSearch', anonymous: true},
'd': {postAction: 'deletePost'}, 'd': {postAction: 'deletePost'},
'e': {postAction: 'editPost'}, 'e': {postAction: 'editPost'},
'end': {handler: 'goToLastPost', anonymous: true}, 'end': {handler: 'goToLastPost', anonymous: true},
@ -142,32 +142,10 @@ export default {
this._changeSection(-1); this._changeSection(-1);
}, },
showBuiltinSearch() { showPageSearch(event) {
if (this.container.lookup('controller:header').get('searchVisible')) { Ember.run(() => {
this.toggleSearch(); this.appEvents.trigger('header:keyboard-trigger', {type: 'page-search', event});
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;
}, },
createTopic() { createTopic() {
@ -182,17 +160,16 @@ export default {
this.container.lookup('controller:topic-progress').send('toggleExpansion', {highlight: true}); this.container.lookup('controller:topic-progress').send('toggleExpansion', {highlight: true});
}, },
toggleSearch() { toggleSearch(event) {
this.container.lookup('controller:header').send('toggleSearch'); this.appEvents.trigger('header:keyboard-trigger', {type: 'search', event});
return false;
}, },
toggleHamburgerMenu() { toggleHamburgerMenu(event) {
this.container.lookup('controller:header').send('toggleMenuPanel', 'hamburgerVisible'); this.appEvents.trigger('header:keyboard-trigger', {type: 'hamburger', event});
}, },
showCurrentUser() { showCurrentUser(event) {
this.container.lookup('controller:header').send('toggleMenuPanel', 'userMenuVisible'); this.appEvents.trigger('header:keyboard-trigger', {type: 'user', event});
}, },
showHelpModal() { showHelpModal() {

View File

@ -9,6 +9,7 @@ import { createWidget, decorateWidget, changeSetting } from 'discourse/widgets/w
import { onPageChange } from 'discourse/lib/page-tracker'; import { onPageChange } from 'discourse/lib/page-tracker';
import { preventCloak } from 'discourse/widgets/post-stream'; import { preventCloak } from 'discourse/widgets/post-stream';
import { h } from 'virtual-dom'; import { h } from 'virtual-dom';
import { addFlagProperty } from 'discourse/components/site-header';
class PluginApi { class PluginApi {
constructor(version, container) { constructor(version, container) {
@ -284,11 +285,20 @@ class PluginApi {
createWidget(name, args) { createWidget(name, args) {
return 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; let _pluginv01;
function getPluginApi(version) { function getPluginApi(version) {
if (version === "0.1" || version === "0.2" || version === "0.3") { version = parseFloat(version);
if (version <= 0.4) {
if (!_pluginv01) { if (!_pluginv01) {
_pluginv01 = new PluginApi(version, Discourse.__container__); _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 * 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 * plugin is coded against. If that API is available, the `apiCodeCallback` function will

View File

@ -3,6 +3,7 @@ import logout from 'discourse/lib/logout';
import showModal from 'discourse/lib/show-modal'; import showModal from 'discourse/lib/show-modal';
import OpenComposer from "discourse/mixins/open-composer"; import OpenComposer from "discourse/mixins/open-composer";
import Category from 'discourse/models/category'; import Category from 'discourse/models/category';
import mobile from 'discourse/lib/mobile';
function unlessReadOnly(method, message) { function unlessReadOnly(method, message) {
return function() { return function() {
@ -25,6 +26,22 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, {
actions: { 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")), logout: unlessReadOnly('_handleLogout', I18n.t("read_only_mode.logout_disabled")),
_collectTitleTokens(tokens) { _collectTitleTokens(tokens) {

View File

@ -75,7 +75,6 @@ const DiscourseRoute = Ember.Route.extend({
}); });
export function cleanDOM() { export function cleanDOM() {
if (window.MiniProfiler) { if (window.MiniProfiler) {
window.MiniProfiler.pageTransition(); window.MiniProfiler.pageTransition();
} }

View File

@ -179,8 +179,9 @@ const TopicRoute = Discourse.Route.extend({
this.searchService.set('searchContext', null); this.searchService.set('searchContext', null);
this.controllerFor('user-card').set('visible', false); this.controllerFor('user-card').set('visible', false);
const topicController = this.controllerFor('topic'), const topicController = this.controllerFor('topic');
postStream = topicController.get('model.postStream'); const postStream = topicController.get('model.postStream');
postStream.cancelFilter(); postStream.cancelFilter();
topicController.set('multiSelect', false); topicController.set('multiSelect', false);
@ -188,11 +189,7 @@ const TopicRoute = Discourse.Route.extend({
this.controllerFor('composer').set('topic', null); this.controllerFor('composer').set('topic', null);
this.screenTrack.stop(); this.screenTrack.stop();
const headerController = this.controllerFor('header'); this.appEvents.trigger('header:hide-topic');
if (headerController) {
headerController.set('topic', null);
headerController.set('showExtraInfo', false);
}
}, },
setupController(controller, model) { setupController(controller, model) {
@ -207,7 +204,6 @@ const TopicRoute = Discourse.Route.extend({
TopicRoute.trigger('setupTopicController', this); TopicRoute.trigger('setupTopicController', this);
this.controllerFor('header').setProperties({ topic: model, showExtraInfo: false });
this.searchService.set('searchContext', model.get('searchContext')); this.searchService.set('searchContext', model.get('searchContext'));
this.controllerFor('composer').set('topic', model); this.controllerFor('composer').set('topic', model);

View File

@ -1,4 +1,11 @@
{{render "header"}} {{site-header canSignUp=canSignUp
showCreateAccount="showCreateAccount"
showLogin="showLogin"
showKeyboard="showKeyboardShortcutsHelp"
toggleMobileView="toggleMobileView"
toggleAnonymous="toggleAnonymous"
logout="logout"
showSearchHelp="showSearchHelp"}}
<div id="main-outlet" class="wrap"> <div id="main-outlet" class="wrap">
<div class="container"> <div class="container">

View File

@ -1,94 +0,0 @@
{{#menu-panel visible=visible}}
{{#if prioritizeFaq}}
{{#menu-links}}
<li class='heading'>
{{#d-link path=faqUrl class="faq-link"}}
{{i18n "faq"}}
<span class='badge badge-notification'>{{i18n "new_item"}}</span>
{{/d-link}}
</li>
{{/menu-links}}
{{/if}}
{{#if currentUser.staff}}
{{#menu-links}}
<li>{{d-link route="admin" class="admin-link" icon="wrench" label="admin_title"}}</li>
<li>
{{#d-link route="adminFlags" class="flagged-posts-link"}}
{{fa-icon "flag"}} {{i18n 'flags_title'}}
{{#if currentUser.site_flagged_posts_count}}
<span title={{i18n 'notifications.total_flagged'}} class='badge-notification flagged-posts'>{{currentUser.site_flagged_posts_count}}</span>
{{/if}}
{{/d-link}}
</li>
{{#if currentUser.show_queued_posts}}
<li>
{{#d-link route='queued-posts'}}
{{i18n "queue.title"}}
{{#if currentUser.post_queue_new_count}}
<span class='badge-notification flagged-posts'>{{currentUser.post_queue_new_count}}</span>
{{/if}}
{{/d-link}}
</li>
{{/if}}
{{#if currentUser.admin}}
<li>{{d-link route="adminSiteSettings" icon="gear" label="admin.site_settings.title"}}</li>
{{/if}}
{{plugin-outlet "hamburger-admin"}}
{{/menu-links}}
{{/if}}
{{#menu-links}}
<li>{{d-link route="discovery.latest" class="latest-topics-link" label="filters.latest.title"}}</li>
{{#if currentUser}}
<li>
{{#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}}
</li>
<li>
{{#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}}
</li>
{{/if}}
<li>{{d-link route="discovery.top" class="top-topics-link" label="filters.top.title"}}</li>
{{#if siteSettings.enable_badges}}
<li>{{d-link route="badges" class="badge-link" label="badges.title"}}</li>
{{/if}}
{{#if showUserDirectoryLink}}
<li>{{d-link route="users" class="user-directory-link" label="directory.title"}}</li>
{{/if}}
{{plugin-outlet "site-map-links"}}
{{plugin-outlet "site-map-links-last"}}
{{/menu-links}}
{{mount-widget widget='hamburger-categories' args=(as-hash categories=categories)}}
<hr>
{{#menu-links omitRule="true"}}
<li>{{d-link route="about" class="about-link" label="about.simple_title"}}</li>
{{#unless prioritizeFaq}}
<li>{{d-link path=faqUrl class="faq-link" label="faq"}}</li>
{{/unless}}
{{#if showKeyboardShortcuts}}
<li>{{d-link action="keyboardShortcuts" class="keyboard-shortcuts-link" label="keyboard_shortcuts_help.title"}}</li>
{{/if}}
{{#if showMobileToggle}}
<li>{{d-link action="toggleMobileView" class="mobile-toggle-link" label=mobileViewLinkTextKey}}</li>
{{/if}}
{{/menu-links}}
{{/menu-panel}}

View File

@ -1,9 +0,0 @@
<a {{action "toggle"}} class='icon' href={{href}} title={{i18n title}} aria-label={{i18n title}} id={{iconId}}>
{{#if showUser}}
{{bound-avatar currentUser "medium"}}
{{else}}
{{fa-icon icon}}
{{/if}}
</a>
{{yield}}

View File

@ -1,21 +0,0 @@
<div class="extra-info-wrapper">
<div {{bind-attr class=":extra-info needsSecondRow:two-rows"}}>
<div class="title-wrapper">
<h1>
{{#if showPrivateMessageGlyph}}
<a href='{{pmPath}}'>
<span class="private-message-glyph">{{fa-icon "envelope"}}</span>
</a>
{{/if}}
{{#if topic.details.loaded}}
{{topic-status topic=topic}}
<a class='topic-link' href='{{unbound topic.url}}' {{action "jumpToTopPost"}}>{{{topic.fancyTitle}}}</a>
{{/if}}
</h1>
{{#if topic.details.loaded}}
{{topic-category topic=topic}}
{{/if}}
</div>
</div>
</div>

View File

@ -1,7 +0,0 @@
<ul class="menu-links columned">
{{yield}}
<div class="clearfix"></div>
</ul>
{{#unless omitRule}}
<hr>
{{/unless}}

View File

@ -1,7 +0,0 @@
{{#if visible}}
<div class='panel-body'>
<div class='panel-body-contents'>
{{yield}}
</div>
</div>
{{/if}}

View File

@ -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"}}
<div class="search-context">
{{#if searchService.searchContext}}
<label>
{{input type="checkbox" name="searchContext" checked=searchService.searchContextEnabled}} {{searchContextDescription}}
</label>
{{/if}}
<a href class="show-help" {{action "showSearchHelp" bubbles=false}}>{{i18n "show_help"}}</a>
<div class='clearfix'></div>
</div>
{{#if loading}}
<div class="searching">{{loading-spinner}}</div>
{{else}}
<div class="results">
{{#if noResults}}
<div class="no-results">
{{i18n "search.no_results"}}
</div>
{{else}}
{{#each content.resultTypes as |resultType|}}
<ul>
<li class="heading row">{{resultType.name}}</li>
{{component resultType.componentName results=resultType.results term=searchService.term}}
</ul>
<div class="no-results">
{{#if resultType.moreUrl}}
<a href={{resultType.moreUrl}} class="filter">{{i18n "show_more"}} {{fa-icon "chevron-down"}}</a>
{{/if}}
{{#if resultType.more}}
<a href class="filter filter-type" {{action "moreOfType" resultType.type bubbles=false}}>{{i18n "show_more"}} {{fa-icon "chevron-down"}}</a>
{{/if}}
</div>
{{/each}}
{{/if}}
</div>
{{/if}}
{{/menu-panel}}

View File

@ -1,7 +0,0 @@
{{#each results as |result|}}
<li>
<a href='{{unbound result.url}}'>
{{category-badge result}}
</a>
</li>
{{/each}}

View File

@ -1,14 +0,0 @@
{{#each results as |result|}}
<li>
<a class='search-link' href='{{unbound result.urlWithNumber}}'>
<span class='topic'>
{{i18n 'search.post_format' post_number=result.post_number username=result.username}}
</span>
{{#unless site.mobileView}}
<span class='blurb'>
{{{unbound result.blurb}}}
</span>
{{/unless}}
</a>
</li>
{{/each}}

View File

@ -1,14 +0,0 @@
{{#each results as |result|}}
<li>
<a class='search-link' href='{{unbound result.url}}'>
<span class='topic'>
{{topic-status topic=result.topic disableActions=true}}<span class='topic-title'>{{{unbound result.topic.fancyTitle}}}</span>{{category-badge result.topic.category}}{{plugin-outlet "search-category"}}
</span>
{{#unless site.mobileView}}
<span class='blurb'>
{{format-age result.created_at}} - {{{unbound result.blurb}}}
</span>
{{/unless}}
</a>
</li>
{{/each}}

View File

@ -1,8 +0,0 @@
{{#each results as |result|}}
<li>
<a href='{{unbound result.path}}'>
{{avatar result imageSize="small"}}
{{unbound result.username}}
</a>
</li>
{{/each}}

View File

@ -1,48 +0,0 @@
{{#menu-panel visible=visible}}
<div class='menu-links-header'>
<ul class='menu-links-row'>
{{#if showDisableAnon}}
<li>{{d-link route='user' model=currentUser class="user-activity-link" icon="user" label="user.profile"}}</li>
<li>{{d-link action="toggleAnon" label="switch_from_anon"}}</li>
{{else}}
<li>{{d-link route='user' model=currentUser class="user-activity-link" icon="user" translateLabel="false" label=currentUser.username}}</li>
{{/if}}
<li class='glyphs'>
{{d-link path=bookmarksPath title="user.bookmarks" icon="bookmark"}}
{{#if siteSettings.enable_private_messages}}
{{d-link path=messagesPath title="user.private_messages" icon="envelope"}}
{{/if}}
{{#if showEnableAnon}}
{{d-link action="toggleAnon" title="switch_to_anon" icon="user-secret"}}
{{/if}}
{{d-link path=preferencesPath title="user.preferences" icon="gear"}}
</li>
</ul>
</div>
<div class='notifications'>
{{#conditional-loading-spinner condition=loadingNotifications containerClass="spinner-container"}}
{{#if notifications}}
<hr>
<ul>
{{#each notifications as |n|}}
{{notification-item notification=n}}
{{/each}}
<li class="read last heading">
{{#d-link path=notificationsPath}}
{{i18n 'notifications.more'}}&hellip;
{{/d-link}}
</li>
</ul>
{{/if}}
{{/conditional-loading-spinner}}
</div>
{{plugin-outlet "user-menu-bottom"}}
<div class='logout-link'>
<hr>
<ul class='menu-links'>
<li>{{d-link action="logout" class="logout" icon="sign-out" label="user.log_out"}}</li>
</ul>
</div>
{{/menu-panel}}

View File

@ -2,5 +2,5 @@
<h3>{{i18n 'composer.similar_topics'}}</h3> <h3>{{i18n 'composer.similar_topics'}}</h3>
<ul class='topics'> <ul class='topics'>
{{search-result-topic results=similarTopics}} {{mount-widget widget="search-result-topic" args=(as-hash results=similarTopics)}}
</ul> </ul>

View File

@ -1,66 +0,0 @@
<div class='wrap'>
<div class='contents clearfix'>
{{home-logo minimized=showExtraInfo}}
{{plugin-outlet "header-after-home-logo"}}
<div class='panel clearfix'>
{{#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}}
<ul class='icons clearfix' role='navigation'>
{{#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}}
<a href='/admin/flags/active' title='{{i18n 'notifications.total_flagged'}}' class='badge-notification flagged-posts'>{{flaggedPostsCount}}</a>
{{/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}}
<a href {{action "showUserMenu"}} class='badge-notification unread-notifications'>{{currentUser.unread_notifications}}</a>
{{/if}}
{{#if currentUser.unread_private_messages}}
<a href {{action "showUserMenu"}} class='badge-notification unread-private-messages'>{{currentUser.unread_private_messages}}</a>
{{/if}}
{{plugin-outlet "header-notifications"}}
{{/header-dropdown}}
{{/if}}
</ul>
{{plugin-outlet "header-before-dropdowns"}}
{{user-menu visible=userMenuVisible logoutAction="logout"}}
{{hamburger-menu visible=hamburgerVisible showKeyboardAction="showKeyboardShortcutsHelp"}}
{{search-menu visible=searchVisible}}
</div>
{{#if showExtraInfo}}
{{header-extra-info topic=topic}}
{{/if}}
</div>
</div>
{{plugin-outlet "header-under-content"}}

View File

@ -14,14 +14,7 @@
</div> </div>
{{/if}} {{/if}}
{{#each n in model}} {{user-notifications-large notifications=model}}
<div {{bind-attr class=":item :notification n.read::unread"}}>
{{notification-item notification=n}}
<span class="time">
{{format-date n.created_at leaveAgo="true"}}
</span>
</div>
{{/each}}
{{#conditional-loading-spinner condition=loading}} {{#conditional-loading-spinner condition=loading}}
{{#unless model.canLoadMore}} {{#unless model.canLoadMore}}

View File

@ -23,5 +23,7 @@
</section> </section>
<section class='user-right'> <section class='user-right'>
{{#load-more class="notification-history user-stream" selector=".user-stream .notification" action="loadMore"}}
{{outlet}} {{outlet}}
{{/load-more}}
</section> </section>

View File

@ -1,6 +1,6 @@
import afterTransition from 'discourse/lib/after-transition'; import afterTransition from 'discourse/lib/after-transition';
import positioningWorkaround from 'discourse/lib/safari-hacks'; 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 { default as computed, on, observes } from 'ember-addons/ember-computed-decorators';
import Composer from 'discourse/models/composer'; import Composer from 'discourse/models/composer';

View File

@ -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());
}

View File

@ -23,6 +23,8 @@ const TopicView = Ember.View.extend(AddCategoryClass, AddArchetypeClass, Scrolli
postStream: Em.computed.alias('topic.postStream'), postStream: Em.computed.alias('topic.postStream'),
archetype: Em.computed.alias('topic.archetype'), archetype: Em.computed.alias('topic.archetype'),
_lastShowTopic: null,
_composeChanged: function() { _composeChanged: function() {
const composerController = Discourse.get('router.composerController'); const composerController = Discourse.get('router.composerController');
composerController.clearState(); composerController.clearState();
@ -73,7 +75,7 @@ const TopicView = Ember.View.extend(AddCategoryClass, AddArchetypeClass, Scrolli
this.resetExamineDockCache(); this.resetExamineDockCache();
// this happens after route exit, stuff could have trickled in // 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'), }.on('willDestroyElement'),
@ -90,6 +92,14 @@ const TopicView = Ember.View.extend(AddCategoryClass, AddArchetypeClass, Scrolli
offset: 0, offset: 0,
hasScrolled: Em.computed.gt("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. // The user has scrolled the window, or it is finished rendering and ready for processing.
scrolled() { scrolled() {
if (this.isDestroyed || this.isDestroying || this._state !== 'inDOM') { if (this.isDestroyed || this.isDestroying || this._state !== 'inDOM') {
@ -106,12 +116,16 @@ const TopicView = Ember.View.extend(AddCategoryClass, AddArchetypeClass, Scrolli
this.set("offset", offset); this.set("offset", offset);
const headerController = this.get('controller.controllers.header'), const topic = this.get('controller.model');
topic = this.get('controller.model'); const showTopic = this.showTopicInHeader(topic, offset);
if (this.get('docAt')) { if (showTopic !== this._lastShowTopic) {
headerController.set('showExtraInfo', offset >= this.get('docAt') || topic.get('postStream.firstPostNotLoaded')); this._lastShowTopic = showTopic;
if (showTopic) {
this.appEvents.trigger('header:show-topic', topic);
} else { } else {
headerController.set('showExtraInfo', topic.get('postStream.firstPostNotLoaded')); this.appEvents.trigger('header:hide-topic');
}
} }
// Trigger a scrolled event // Trigger a scrolled event

View File

@ -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']
});

View File

@ -61,6 +61,7 @@ createWidget('action-link', {
createWidget('actions-summary-item', { createWidget('actions-summary-item', {
tagName: 'div.post-action', tagName: 'div.post-action',
buildKey: (attrs) => `actions-summary-item-${attrs.id}`,
defaultState() { defaultState() {
return { users: [] }; return { users: [] };

View File

@ -14,6 +14,8 @@ export default createWidget('button', {
let title; let title;
if (attrs.title) { if (attrs.title) {
title = I18n.t(attrs.title, attrs.titleOptions); title = I18n.t(attrs.title, attrs.titleOptions);
} else if (attrs.label) {
title = I18n.t(attrs.label, attrs.labelOptions);
} }
const attributes = { "aria-label": title, title }; const attributes = { "aria-label": title, title };

View File

@ -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;
};

View File

@ -5,7 +5,7 @@ createWidget('hamburger-category', {
tagName: 'li.category-link', tagName: 'li.category-link',
html(c) { 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); const unreadTotal = parseInt(c.get('unreadTopics'), 10) + parseInt(c.get('newTopics'), 10);
if (unreadTotal) { if (unreadTotal) {

View File

@ -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');
}
});

View File

@ -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'));
}
}
});

View File

@ -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;
}
}
});

View File

@ -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;
}
});

View File

@ -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;
};

View File

@ -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));
}
});

View File

@ -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()));
}
});

View File

@ -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: `<div>${Discourse.Emoji.unescape(this.text())}</div>` });
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());
}
});

View File

@ -7,6 +7,7 @@ const MAX_GUTTER_LINKS = 5;
export default createWidget('post-gutter', { export default createWidget('post-gutter', {
tagName: 'div.gutter', tagName: 'div.gutter',
buildKey: (attrs) => `post-gutter-${attrs.id}`,
defaultState() { defaultState() {
return { collapsed: true }; return { collapsed: true };

View File

@ -51,6 +51,7 @@ createWidget('select-post', {
createWidget('reply-to-tab', { createWidget('reply-to-tab', {
tagName: 'a.reply-to-tab', tagName: 'a.reply-to-tab',
buildKey: attrs => `reply-to-tab-${attrs.id}`,
defaultState() { defaultState() {
return { loading: false }; return { loading: false };
@ -61,7 +62,7 @@ createWidget('reply-to-tab', {
return [iconNode('mail-forward'), return [iconNode('mail-forward'),
' ', ' ',
avatarImg.call(this,'small',{ avatarImg('small', {
template: attrs.replyToAvatarTemplate, template: attrs.replyToAvatarTemplate,
username: attrs.replyToUsername username: attrs.replyToUsername
}), }),

View File

@ -4,9 +4,13 @@ export default class RawHtml {
} }
init() { init() {
return $(this.html)[0]; const $html = $(this.html);
this.decorate($html);
return $html[0];
} }
decorate() { }
update(prev) { update(prev) {
if (prev.html === this.html) { return; } if (prev.html === this.html) { return; }
return this.init(); return this.init();

View File

@ -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);
}
}
});

View File

@ -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: `<span>${html}</span>` });
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)
];
});
}
});

View File

@ -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);
}
}
});

View File

@ -125,6 +125,7 @@ createWidget('topic-map-link', {
createWidget('topic-map-expanded', { createWidget('topic-map-expanded', {
tagName: 'section.topic-map-expanded', tagName: 'section.topic-map-expanded',
buildKey: attrs => `topic-map-expanded-${attrs.id}`,
defaultState() { defaultState() {
return { allLinksShown: false }; return { allLinksShown: false };

View File

@ -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;
}
});

View File

@ -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');
}
});

View File

@ -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));
}
});

View File

@ -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;
}
});

View File

@ -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 { h } from 'virtual-dom';
import DecoratorHelper from 'discourse/widgets/decorator-helper'; import DecoratorHelper from 'discourse/widgets/decorator-helper';
@ -66,6 +66,11 @@ function drawWidget(builder, attrs, state) {
if (this.buildAttributes) { if (this.buildAttributes) {
properties.attributes = this.buildAttributes(attrs); properties.attributes = this.buildAttributes(attrs);
} }
if (this.keyUp) {
properties['widget-key-up'] = new WidgetKeyUpHook(this);
}
if (this.clickOutside) { if (this.clickOutside) {
properties['widget-click-outside'] = new WidgetClickOutsideHook(this); properties['widget-click-outside'] = new WidgetClickOutsideHook(this);
} }
@ -119,9 +124,17 @@ export default class Widget {
this.key = this.buildKey ? this.buildKey(attrs) : null; 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.site = container.lookup('site:main');
this.siteSettings = container.lookup('site-settings:main'); this.siteSettings = container.lookup('site-settings:main');
this.currentUser = container.lookup('current-user:main'); this.currentUser = container.lookup('current-user:main');
this.capabilities = container.lookup('capabilities:main');
this.store = container.lookup('store:main'); this.store = container.lookup('store:main');
this.appEvents = container.lookup('app-events:main'); this.appEvents = container.lookup('app-events:main');
this.keyValueStore = container.lookup('key-value-store:main'); this.keyValueStore = container.lookup('key-value-store:main');
@ -143,7 +156,7 @@ export default class Widget {
} }
render(prev) { render(prev) {
if (prev && prev.state) { if (prev && prev.key && prev.key === this.key) {
this.state = prev.state; this.state = prev.state;
} else { } else {
this.state = this.defaultState(this.attrs, this.state); this.state = this.defaultState(this.attrs, this.state);
@ -166,7 +179,7 @@ export default class Widget {
const refreshAction = dirtyOpts.onRefresh; const refreshAction = dirtyOpts.onRefresh;
if (refreshAction) { if (refreshAction) {
this.sendWidgetAction(refreshAction); this.sendWidgetAction(refreshAction, dirtyOpts.refreshArg);
} }
} }
@ -243,7 +256,7 @@ export default class Widget {
if (target) { if (target) {
// TODO: Use ember closure actions // TODO: Use ember closure actions
const actions = target._actions || target.actionHooks; const actions = target._actions || target.actionHooks || {};
const method = actions[actionName]; const method = actions[actionName];
if (method) { if (method) {
promise = method.call(target, param); promise = method.call(target, param);
@ -276,6 +289,16 @@ export default class Widget {
return result; return result;
} }
sendWidgetEvent(name) {
const methodName = `${name}Event`;
return this.rerenderResult(() => {
const widget = this._findAncestorWithProperty(methodName);
if (widget) {
return widget[methodName]();
}
});
}
sendWidgetAction(name, param) { sendWidgetAction(name, param) {
return this.rerenderResult(() => { return this.rerenderResult(() => {
const widget = this._findAncestorWithProperty(name); const widget = this._findAncestorWithProperty(name);

View File

@ -63,12 +63,11 @@
//= require ./discourse/components/combo-box //= require ./discourse/components/combo-box
//= require ./discourse/components/edit-category-panel //= require ./discourse/components/edit-category-panel
//= require ./discourse/views/button //= require ./discourse/views/button
//= require ./discourse/components/search-result
//= require ./discourse/components/dropdown-button //= require ./discourse/components/dropdown-button
//= require ./discourse/components/notifications-button //= require ./discourse/components/notifications-button
//= require ./discourse/components/topic-notifications-button //= require ./discourse/components/topic-notifications-button
//= require ./discourse/lib/link-mentions //= require ./discourse/lib/link-mentions
//= require ./discourse/views/header //= require ./discourse/components/site-header
//= require ./discourse/lib/utilities //= require ./discourse/lib/utilities
//= require ./discourse/dialects/dialect //= require ./discourse/dialects/dialect
//= require ./discourse/lib/emoji/emoji //= require ./discourse/lib/emoji/emoji

View File

@ -131,7 +131,7 @@
right: 65px; right: 65px;
} }
} }
.flagged-posts { .flagged-posts, .queued-posts {
background: $danger; background: $danger;
} }
} }

View File

@ -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");
});
});

View File

@ -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');
});
});

View File

@ -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");
});
});

View File

@ -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");
});
});

View File

@ -12,6 +12,7 @@ test("search", (assert) => {
}); });
fillIn('#search-term', 'dev'); fillIn('#search-term', 'dev');
keyEvent('#search-term', 'keyup', 16);
andThen(() => { andThen(() => {
assert.ok(exists('.search-menu .results ul li'), 'it shows results'); assert.ok(exists('.search-menu .results ul li'), 'it shows results');
}); });

View File

@ -1,65 +0,0 @@
import componentTest from 'helpers/component-test';
moduleForComponent('menu-panel', {integration: true});
componentTest('as a dropdown', {
template: `
<div id='outside-area'>click me</div>
<div class='menu-selected'></div>
{{#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: `
<div id='outside-area'>click me</div>
<div class='menu-selected'></div>
{{#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);
});
}
});

View File

@ -1,7 +1,7 @@
import { blank, present } from 'helpers/qunit-helpers'; import { blank, present } from 'helpers/qunit-helpers';
moduleFor('controller:topic', 'controller:topic', { 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'] 'controller:topic-progress', 'controller:application']
}); });

View File

@ -1,6 +1,7 @@
import AppEvents from 'discourse/lib/app-events'; import AppEvents from 'discourse/lib/app-events';
import createStore from 'helpers/create-store'; import createStore from 'helpers/create-store';
import { autoLoadModules } from 'discourse/initializers/auto-load-modules'; import { autoLoadModules } from 'discourse/initializers/auto-load-modules';
import TopicTrackingState from 'discourse/models/topic-tracking-state';
export default function(name, opts) { export default function(name, opts) {
opts = opts || {}; opts = opts || {};
@ -22,11 +23,19 @@ export default function(name, opts) {
autoLoadModules(); autoLoadModules();
if (opts.setup) {
const store = createStore(); const store = createStore();
this.currentUser = Discourse.User.create(); if (!opts.anonymous) {
this.container.register('store:main', store, { instantiate: false }); const currentUser = Discourse.User.create({ username: 'eviltrout' });
this.currentUser = currentUser;
this.container.register('current-user:main', this.currentUser, { instantiate: false }); 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); opts.setup.call(this, store);
} }

View File

@ -2,7 +2,7 @@
import sessionFixtures from 'fixtures/session-fixtures'; import sessionFixtures from 'fixtures/session-fixtures';
import siteFixtures from 'fixtures/site-fixtures'; import siteFixtures from 'fixtures/site-fixtures';
import HeaderView from 'discourse/views/header'; import HeaderComponent from 'discourse/components/site-header';
function currentUser() { function currentUser() {
return Discourse.User.create(sessionFixtures['/session/current.json'].current_user); return Discourse.User.create(sessionFixtures['/session/current.json'].current_user);
@ -41,7 +41,7 @@ function acceptance(name, options) {
Discourse.Utilities.avatarImg = () => ""; Discourse.Utilities.avatarImg = () => "";
// For now don't do scrolling stuff in Test Mode // 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; var siteJson = siteFixtures['site.json'].site;
if (options) { if (options) {

View File

@ -7,8 +7,8 @@ widgetTest('listing actions', {
setup() { setup() {
this.set('args', { this.set('args', {
actionsSummary: [ actionsSummary: [
{action: 'off_topic', description: 'very off topic'}, {id: 1, action: 'off_topic', description: 'very off topic'},
{action: 'spam', description: 'suspicious message'} {id: 2, action: 'spam', description: 'suspicious message'}
] ]
}); });
}, },

View File

@ -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);
}
});

View File

@ -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);
});
}
});

View File

@ -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 bigLogo = '/images/d-logo-sketch.png?test';
const smallLogo = '/images/d-logo-sketch-small.png?test'; const smallLogo = '/images/d-logo-sketch-small.png?test';
const mobileLogo = '/images/d-logo-sketch.png?mobile'; const mobileLogo = '/images/d-logo-sketch.png?mobile';
const title = "Cool Forum"; const title = "Cool Forum";
componentTest('basics', { widgetTest('basics', {
template: '{{home-logo minimized=minimized}}', template: '{{mount-widget widget="home-logo" args=args}}',
setup() { setup() {
this.siteSettings.logo_url = bigLogo; this.siteSettings.logo_url = bigLogo;
this.siteSettings.logo_small_url= smallLogo; this.siteSettings.logo_small_url= smallLogo;
this.siteSettings.title = title; this.siteSettings.title = title;
this.set('minimized', false); this.set('args', { minimized: false });
}, },
test(assert) { test(assert) {
@ -23,23 +23,32 @@ componentTest('basics', {
assert.ok(this.$('img#site-logo.logo-big').length === 1); assert.ok(this.$('img#site-logo.logo-big').length === 1);
assert.equal(this.$('#site-logo').attr('src'), bigLogo); assert.equal(this.$('#site-logo').attr('src'), bigLogo);
assert.equal(this.$('#site-logo').attr('alt'), title); 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', { widgetTest('basics - minmized', {
template: '{{home-logo minimized=minimized}}', 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() { setup() {
this.siteSettings.logo_url = ''; this.siteSettings.logo_url = '';
this.siteSettings.logo_small_url = ''; this.siteSettings.logo_small_url = '';
this.siteSettings.title = title; this.siteSettings.title = title;
this.set('minimized', false); this.set('args', { minimized: false });
}, },
test(assert) { test(assert) {
@ -47,16 +56,25 @@ componentTest('no logo', {
assert.ok(this.$('h2#site-text-logo.text-logo').length === 1); assert.ok(this.$('h2#site-text-logo.text-logo').length === 1);
assert.equal(this.$('#site-text-logo').text(), title); assert.equal(this.$('#site-text-logo').text(), title);
this.set('minimized', true);
andThen(() => {
assert.ok(this.$('i.fa-home').length === 1);
});
} }
}); });
componentTest('mobile logo', { widgetTest('no logo - minimized', {
template: "{{home-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('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() { setup() {
this.siteSettings.mobile_logo_url = mobileLogo; this.siteSettings.mobile_logo_url = mobileLogo;
this.siteSettings.logo_small_url= smallLogo; this.siteSettings.logo_small_url= smallLogo;
@ -69,8 +87,8 @@ componentTest('mobile logo', {
} }
}); });
componentTest('mobile without logo', { widgetTest('mobile without logo', {
template: "{{home-logo}}", template: '{{mount-widget widget="home-logo" args=args}}',
setup() { setup() {
this.siteSettings.logo_url = bigLogo; this.siteSettings.logo_url = bigLogo;
this.site.mobileView = true; this.site.mobileView = true;
@ -81,10 +99,3 @@ componentTest('mobile without logo', {
assert.equal(this.$('#site-logo').attr('src'), bigLogo); 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');
}
});

View File

@ -6,6 +6,7 @@ widgetTest("duplicate links", {
template: '{{mount-widget widget="post-gutter" args=args}}', template: '{{mount-widget widget="post-gutter" args=args}}',
setup() { setup() {
this.set('args', { this.set('args', {
id: 2,
links: [ links: [
{ title: "Evil Trout Link", url: "http://eviltrout.com" }, { title: "Evil Trout Link", url: "http://eviltrout.com" },
{ title: "Evil Trout Link", url: "http://dupe.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}}', template: '{{mount-widget widget="post-gutter" args=args}}',
setup() { setup() {
this.set('args', { this.set('args', {
id: 1,
links: [ links: [
{ title: "Link 1", url: "http://eviltrout.com?1" }, { title: "Link 1", url: "http://eviltrout.com?1" },
{ title: "Link 2", url: "http://eviltrout.com?2" }, { title: "Link 2", url: "http://eviltrout.com?2" },

View File

@ -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);
});
}
});

View File

@ -89,6 +89,7 @@ widgetTest('widget state', {
setup() { setup() {
createWidget('state-test', { createWidget('state-test', {
tagName: 'button.test', tagName: 'button.test',
buildKey: () => `button-test`,
defaultState() { defaultState() {
return { clicks: 0 }; return { clicks: 0 };
@ -121,6 +122,7 @@ widgetTest('widget update with promise', {
setup() { setup() {
createWidget('promise-test', { createWidget('promise-test', {
tagName: 'button.test', tagName: 'button.test',
buildKey: () => 'promise-test',
html(attrs, state) { html(attrs, state) {
return state.name || "No name"; return state.name || "No name";