Migrate user card to use components

This commit is contained in:
Robin Ward 2016-11-17 15:40:04 -05:00
parent 742f01f82c
commit 56642bbde3
5 changed files with 408 additions and 411 deletions

View File

@ -0,0 +1,275 @@
import { wantsNewWindow } from 'discourse/lib/intercept-click';
import { propertyNotEqual, setting } from 'discourse/lib/computed';
import CleansUp from 'discourse/mixins/cleans-up';
import afterTransition from 'discourse/lib/after-transition';
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
import DiscourseURL from 'discourse/lib/url';
import User from 'discourse/models/user';
const clickOutsideEventName = "mousedown.outside-user-card";
const clickDataExpand = "click.discourse-user-card";
const clickMention = "click.discourse-user-mention";
export default Ember.Component.extend(CleansUp, {
elementId: 'user-card',
classNameBindings: ['visible:show', 'showBadges', 'hasCardBadgeImage'],
allowBackgrounds: setting('allow_profile_backgrounds'),
postStream: Ember.computed.alias('topic.postStream'),
enoughPostsForFiltering: Ember.computed.gte('topicPostCount', 2),
viewingTopic: Ember.computed.match('currentPath', /^topic\./),
viewingAdmin: Ember.computed.match('currentPath', /^admin\./),
showFilter: Ember.computed.and('viewingTopic', 'postStream.hasNoFilters', 'enoughPostsForFiltering'),
showName: propertyNotEqual('user.name', 'user.username'),
hasUserFilters: Ember.computed.gt('postStream.userFilters.length', 0),
isSuspended: Ember.computed.notEmpty('user.suspend_reason'),
showBadges: setting('enable_badges'),
showMoreBadges: Ember.computed.gt('moreBadgesCount', 0),
showDelete: Ember.computed.and("viewingAdmin", "showName", "user.canBeDeleted"),
linkWebsite: Ember.computed.not('user.isBasic'),
hasLocationOrWebsite: Ember.computed.or('user.location', 'user.website_name'),
visible: false,
user: null,
username: null,
avatar: null,
userLoading: null,
cardTarget: null,
post: null,
// If inside a topic
topicPostCount: null,
@computed('user.name')
nameFirst(name) {
return !this.siteSettings.prioritize_username_in_ux && name && name.trim().length > 0;
},
@computed('user.user_fields.@each.value')
publicUserFields() {
const siteUserFields = this.site.get('user_fields');
if (!Ember.isEmpty(siteUserFields)) {
const userFields = this.get('user.user_fields');
return siteUserFields.filterBy('show_on_user_card', true).sortBy('position').map(field => {
Ember.set(field, 'dasherized_name', field.get('name').dasherize());
const value = userFields ? userFields[field.get('id')] : null;
return Ember.isEmpty(value) ? null : Ember.Object.create({ value, field });
}).compact();
}
},
@computed("user.trust_level")
removeNoFollow(trustLevel) {
return trustLevel > 2 && !this.siteSettings.tl3_links_no_follow;
},
@computed('user.badge_count', 'user.featured_user_badges.length')
moreBadgesCount: (badgeCount, badgeLength) => badgeCount - badgeLength,
@computed('user.card_badge.image')
hasCardBadgeImage: image => image && image.indexOf('fa-') !== 0,
@observes('user.card_background')
addBackground() {
const url = this.get('user.card_background');
if (!this.get('allowBackgrounds')) { return; }
const $this = this.$();
if (!$this) { return; }
if (Ember.isEmpty(url)) {
$this.css('background-image', '').addClass('no-bg');
} else {
$this.css('background-image', `url(${Discourse.getURLWithCDN(url)})`).removeClass('no-bg');
}
},
_show(username, $target) {
// No user card for anon
if (this.siteSettings.hide_user_profiles_from_public && !this.currentUser) {
return true;
}
// XSS protection (should be encapsulated)
username = username.toString().replace(/[^A-Za-z0-9_\.\-]/g, "");
// Don't show on mobile
if (this.site.mobileView) {
DiscourseURL.routeTo(`/users/${username}`);
return false;
}
const currentUsername = this.get('username');
if (username === currentUsername && this.get('userLoading') === username) {
return;
}
const postId = $target.parents('article').data('post-id');
const wasVisible = this.get('visible');
const previousTarget = this.get('cardTarget');
const target = $target[0];
if (wasVisible) {
this._close();
if (target === previousTarget) { return; }
}
const post = this.get('viewingTopic') && postId ? this.get('postStream').findLoadedPost(postId) : null;
this.setProperties({ username, userLoading: username, cardTarget: target, post });
const args = { stats: false };
args.include_post_count_for = this.get('topic.id');
args.skip_track_visit = true;
User.findByUsername(username, args).then(user => {
if (user.topic_post_count) {
this.set('topicPostCount', user.topic_post_count[args.include_post_count_for]);
}
this.setProperties({ user, avatar: user, visible: true });
this._positionCard($target);
}).catch(() => this._close()).finally(() => this.set('userLoading', null));
return false;
},
didInsertElement() {
this._super();
afterTransition(this.$(), this._hide.bind(this));
$('html').off(clickOutsideEventName)
.on(clickOutsideEventName, (e) => {
if (this.get('visible')) {
const $target = $(e.target);
if ($target.closest('[data-user-card]').data('userCard') ||
$target.closest('a.mention').length > 0 ||
$target.closest('#user-card').length > 0) {
return;
}
this._close();
}
return true;
});
$('#main-outlet').on(clickDataExpand, '[data-user-card]', (e) => {
if (wantsNewWindow(e)) { return; }
const $target = $(e.currentTarget);
return this._show($target.data('user-card'), $target);
});
$('#main-outlet').on(clickMention, 'a.mention', (e) => {
if (wantsNewWindow(e)) { return; }
const $target = $(e.target);
return this._show($target.text().replace(/^@/, ''), $target);
});
},
_positionCard(target) {
const rtl = ($('html').css('direction')) === 'rtl';
if (!target) { return; }
const width = this.$().width();
Ember.run.schedule('afterRender', () => {
if (target) {
let position = target.offset();
if (position) {
if (rtl) { // The site direction is rtl
position.right = $(window).width() - position.left + 10;
position.left = 'auto';
let overage = ($(window).width() - 50) - (position.right + width);
if (overage < 0) {
position.right += overage;
position.top += target.height() + 48;
}
} else { // The site direction is ltr
position.left += target.width() + 10;
let overage = ($(window).width() - 50) - (position.left + width);
if (overage < 0) {
position.left += overage;
position.top += target.height() + 48;
}
}
position.top -= $('#main-outlet').offset().top;
this.$().css(position);
}
// After the card is shown, focus on the first link
//
// note: we DO NOT use afterRender here cause _positionCard may
// run afterwards, if we allowed this to happen the usercard
// may be offscreen and we may scroll all the way to it on focus
Ember.run.next(null, () => this.$('a:first').focus() );
}
});
},
_hide() {
if (!this.get('visible')) {
this.$().css({left: -9999, top: -9999});
}
},
_close() {
this.setProperties({
visible: false,
user: null,
username: null,
avatar: null,
userLoading: null,
cardTarget: null,
post: null,
topicPostCount: null
});
},
cleanUp() {
this._close();
},
keyUp(e) {
if (e.keyCode === 27) { // ESC
const target = this.get('cardTarget');
this._close();
target.focus();
}
},
willDestroyElement() {
this._super();
$('html').off(clickOutsideEventName);
$('#main').off(clickDataExpand).off(clickMention);
},
actions: {
cancelFilter() {
const postStream = this.get('postStream');
postStream.cancelFilter();
postStream.refresh();
this._close();
},
composePrivateMessage(...args) {
this.sendAction('composePrivateMessage', ...args);
},
togglePosts() {
this.sendAction('togglePosts', this.get('user'));
this._close();
},
deleteUser() {
this.sendAction('deleteUser', this.get('user'));
},
showUser() {
this.sendAction('showUser', this.get('user'));
this._close();
}
}
});

View File

@ -1,151 +1,15 @@
import DiscourseURL from 'discourse/lib/url';
import { propertyNotEqual, setting } from 'discourse/lib/computed';
import computed from 'ember-addons/ember-computed-decorators';
export default Ember.Controller.extend({
topic: Ember.inject.controller(),
application: Ember.inject.controller(),
visible: false,
user: null,
username: null,
avatar: null,
userLoading: null,
cardTarget: null,
post: null,
// If inside a topic
topicPostCount: null,
postStream: Em.computed.alias('topic.model.postStream'),
enoughPostsForFiltering: Em.computed.gte('topicPostCount', 2),
viewingTopic: Em.computed.match('application.currentPath', /^topic\./),
viewingAdmin: Em.computed.match('application.currentPath', /^admin\./),
showFilter: Em.computed.and('viewingTopic', 'postStream.hasNoFilters', 'enoughPostsForFiltering'),
showName: propertyNotEqual('user.name', 'user.username'),
hasUserFilters: Em.computed.gt('postStream.userFilters.length', 0),
isSuspended: Em.computed.notEmpty('user.suspend_reason'),
showBadges: setting('enable_badges'),
showMoreBadges: Em.computed.gt('moreBadgesCount', 0),
showDelete: Em.computed.and("viewingAdmin", "showName", "user.canBeDeleted"),
linkWebsite: Em.computed.not('user.isBasic'),
hasLocationOrWebsite: Em.computed.or('user.location', 'user.website_name'),
@computed('user.name')
nameFirst(name) {
return !this.get('siteSettings.prioritize_username_in_ux') && name && name.trim().length > 0;
},
@computed('user.user_fields.@each.value')
publicUserFields() {
const siteUserFields = this.site.get('user_fields');
if (!Ember.isEmpty(siteUserFields)) {
const userFields = this.get('user.user_fields');
return siteUserFields.filterBy('show_on_user_card', true).sortBy('position').map(field => {
Ember.set(field, 'dasherized_name', field.get('name').dasherize());
const value = userFields ? userFields[field.get('id')] : null;
return Ember.isEmpty(value) ? null : Ember.Object.create({ value, field });
}).compact();
}
},
@computed("user.trust_level")
removeNoFollow(trustLevel) {
return trustLevel > 2 && !this.siteSettings.tl3_links_no_follow;
},
moreBadgesCount: function() {
return this.get('user.badge_count') - this.get('user.featured_user_badges.length');
}.property('user.badge_count', 'user.featured_user_badges.[]'),
hasCardBadgeImage: function() {
const img = this.get('user.card_badge.image');
return img && img.indexOf('fa-') !== 0;
}.property('user.card_badge.image'),
show(username, postId, target) {
// XSS protection (should be encapsulated)
username = username.toString().replace(/[^A-Za-z0-9_\.\-]/g, "");
// No user card for anon
if (this.siteSettings.hide_user_profiles_from_public && !this.currentUser) {
return;
}
// Don't show on mobile
if (this.site.mobileView) {
const url = "/users/" + username;
DiscourseURL.routeTo(url);
return;
}
const currentUsername = this.get('username'),
wasVisible = this.get('visible'),
previousTarget = this.get('cardTarget'),
post = this.get('viewingTopic') && postId ? this.get('postStream').findLoadedPost(postId) : null;
if (username === currentUsername && this.get('userLoading') === username) {
// debounce
return;
}
if (wasVisible) {
this.close();
if (target === previousTarget) {
return; // Same target, close it without loading the new user card
}
}
this.setProperties({ username, userLoading: username, cardTarget: target, post });
const args = { stats: false };
args.include_post_count_for = this.get('topic.model.id');
args.skip_track_visit = true;
return Discourse.User.findByUsername(username, args).then((user) => {
if (user.topic_post_count) {
this.set('topicPostCount', user.topic_post_count[args.include_post_count_for]);
}
this.setProperties({ user, avatar: user, visible: true });
}).catch((error) => {
this.close();
throw error;
}).finally(() => {
this.set('userLoading', null);
});
},
close() {
this.setProperties({
visible: false,
user: null,
username: null,
avatar: null,
userLoading: null,
cardTarget: null,
post: null,
topicPostCount: null
});
},
actions: {
togglePosts(user) {
const topicController = this.get('topic');
topicController.send('toggleParticipant', user);
this.close();
},
cancelFilter() {
const postStream = this.get('postStream');
postStream.cancelFilter();
postStream.refresh();
this.close();
},
showUser() {
this.transitionToRoute('user', this.get('user'));
this.close();
showUser(user) {
this.transitionToRoute('user', user);
}
}
});

View File

@ -0,0 +1,124 @@
{{#if visible}}
<div class="card-content">
<div class="user-card-avatar">
<a href={{user.path}} {{action "showUser"}} class="card-huge-avatar">{{bound-avatar avatar "huge"}}</a>
{{#if user.primary_group_name}}
{{mount-widget widget="avatar-flair" args=user}}
{{/if}}
</div>
<div class="names">
<span>
<h1 class="{{staff}} {{new_user}} {{if nameFirst "full-name" "username"}}">
<a href={{user.path}} {{action "showUser"}}>{{if nameFirst user.name username}} {{user-status user currentUser=currentUser}}</a>
</h1>
{{#unless nameFirst}}
{{#if user.name}}
<h2 class='full-name'>{{user.name}}</h2>
{{/if}}
{{else}}
<h2 class='username'>{{username}}</h2>
{{/unless}}
{{#if user.title}}
<h2>{{user.title}}</h2>
{{/if}}
{{plugin-outlet "user-card-post-names"}}
</span>
</div>
<ul class="usercard-controls">
{{#if user.can_send_private_message_to_user}}
<li><a class='btn btn-primary' {{action "composePrivateMessage" user post}}>{{fa-icon "envelope"}}{{i18n 'user.private_message'}}</a></li>
{{/if}}
{{#if showFilter}}
<li><a class='btn' href {{action "togglePosts" user}}>{{fa-icon "filter"}}{{i18n 'topic.filter_to' username=username count=topicPostCount}}</a></li>
{{/if}}
{{#if hasUserFilters}}
<li><a class='btn' href {{action "cancelFilter"}}>{{fa-icon "times"}}{{i18n 'topic.filters.cancel'}}</a></li>
{{/if}}
{{#if showDelete}}
<li><a class='btn btn-danger' href {{action "deleteUser" user}}>{{fa-icon "exclamation-triangle"}}{{i18n 'admin.user.delete'}}</a></li>
{{/if}}
</ul>
{{#if isSuspended}}
<div class='suspended'>
{{fa-icon "ban"}}
<b>{{i18n 'user.suspended_notice' date=user.suspendedTillDate}}</b><br/>
<b>{{i18n 'user.suspended_reason'}}</b> {{user.suspend_reason}}
</div>
{{else}}
{{#if user.bio_cooked}}<div class='bio'>{{text-overflow class="overflow" text=user.bio_excerpt}}</div>{{/if}}
{{/if}}
{{#if user.card_badge}}
{{#link-to 'badges.show' user.card_badge class="card-badge" title=user.card_badge.name}}
{{icon-or-image user.card_badge.image title=user.card_badge.name}}
{{/link-to}}
{{/if}}
{{#if hasLocationOrWebsite}}
<div class="location-and-website">
{{#if user.location}}
<span class='location'>{{fa-icon "map-marker"}} {{user.location}}</span>
{{/if}}
{{#if user.website_name}}
<span class='website-name'>
{{fa-icon "globe"}}
{{#if linkWebsite}}
<a href={{user.website}} rel={{unless removeNoFollow 'nofollow'}} target="_blank">{{user.website_name}}</a>
{{else}}
<span title={{user.website}}>{{user.website_name}}</span>
{{/if}}
</span>
{{/if}}
{{plugin-outlet "user-card-location-and-website"}}
</div>
{{/if}}
{{#if user}}
<div class="metadata">
{{#if user.last_posted_at}}
<h3><span class='desc'>{{i18n 'last_post'}}</span> {{format-date user.last_posted_at leaveAgo="true"}}</h3>
{{/if}}
<h3><span class='desc'>{{i18n 'joined'}}</span> {{format-date user.created_at leaveAgo="true"}}</h3>
{{plugin-outlet "user-card-metadata"}}
</div>
{{/if}}
{{#if publicUserFields}}
<div class="public-user-fields">
{{#each publicUserFields as |uf|}}
{{#if uf.value}}
<div class="public-user-field {{uf.field.dasherized_name}}">
<span class="user-field-name">{{uf.field.name}}:</span>
<span class="user-field-value">{{uf.value}}</span>
</div>
{{/if}}
{{/each}}
</div>
{{/if}}
{{#if showBadges}}
<div class="badge-section">
{{#each user.featured_user_badges as |ub|}}
{{user-badge badge=ub.badge user=user}}
{{/each}}
{{#if showMoreBadges}}
{{#link-to 'user.badges' user class="btn more-user-badges"}}
{{i18n 'badges.more_badges' count=moreBadgesCount}}
{{/link-to}}
{{/if}}
</div>
{{/if}}
</div>
{{/if}}

View File

@ -1,124 +1,7 @@
{{#if visible}}
<div class="card-content">
<div class="user-card-avatar">
<a href={{user.path}} {{action "showUser"}} class="card-huge-avatar">{{bound-avatar avatar "huge"}}</a>
{{#if user.primary_group_name}}
{{mount-widget widget="avatar-flair" args=user}}
{{/if}}
</div>
<div class="names">
<span>
<h1 class="{{staff}} {{new_user}} {{if nameFirst "full-name" "username"}}">
<a href={{user.path}} {{action "showUser"}}>{{if nameFirst user.name username}} {{user-status user currentUser=currentUser}}</a>
</h1>
{{#unless nameFirst}}
{{#if user.name}}
<h2 class='full-name'>{{user.name}}</h2>
{{/if}}
{{else}}
<h2 class='username'>{{username}}</h2>
{{/unless}}
{{#if user.title}}
<h2>{{user.title}}</h2>
{{/if}}
{{plugin-outlet "user-card-post-names"}}
</span>
</div>
<ul class="usercard-controls">
{{#if user.can_send_private_message_to_user}}
<li><a class='btn btn-primary' {{action "composePrivateMessage" user post}}>{{fa-icon "envelope"}}{{i18n 'user.private_message'}}</a></li>
{{/if}}
{{#if showFilter}}
<li><a class='btn' href {{action "togglePosts" user}}>{{fa-icon "filter"}}{{i18n 'topic.filter_to' username=username count=topicPostCount}}</a></li>
{{/if}}
{{#if hasUserFilters}}
<li><a class='btn' href {{action "cancelFilter"}}>{{fa-icon "times"}}{{i18n 'topic.filters.cancel'}}</a></li>
{{/if}}
{{#if showDelete}}
<li><a class='btn btn-danger' href {{action "deleteUser" user}}>{{fa-icon "exclamation-triangle"}}{{i18n 'admin.user.delete'}}</a></li>
{{/if}}
</ul>
{{#if isSuspended}}
<div class='suspended'>
{{fa-icon "ban"}}
<b>{{i18n 'user.suspended_notice' date=user.suspendedTillDate}}</b><br/>
<b>{{i18n 'user.suspended_reason'}}</b> {{user.suspend_reason}}
</div>
{{else}}
{{#if user.bio_cooked}}<div class='bio'>{{text-overflow class="overflow" text=user.bio_excerpt}}</div>{{/if}}
{{/if}}
{{#if user.card_badge}}
{{#link-to 'badges.show' user.card_badge class="card-badge" title=user.card_badge.name}}
{{icon-or-image user.card_badge.image title=user.card_badge.name}}
{{/link-to}}
{{/if}}
{{#if hasLocationOrWebsite}}
<div class="location-and-website">
{{#if user.location}}
<span class='location'>{{fa-icon "map-marker"}} {{user.location}}</span>
{{/if}}
{{#if user.website_name}}
<span class='website-name'>
{{fa-icon "globe"}}
{{#if linkWebsite}}
<a href={{user.website}} rel={{unless removeNoFollow 'nofollow'}} target="_blank">{{user.website_name}}</a>
{{else}}
<span title={{user.website}}>{{user.website_name}}</span>
{{/if}}
</span>
{{/if}}
{{plugin-outlet "user-card-location-and-website"}}
</div>
{{/if}}
{{#if user}}
<div class="metadata">
{{#if user.last_posted_at}}
<h3><span class='desc'>{{i18n 'last_post'}}</span> {{format-date user.last_posted_at leaveAgo="true"}}</h3>
{{/if}}
<h3><span class='desc'>{{i18n 'joined'}}</span> {{format-date user.created_at leaveAgo="true"}}</h3>
{{plugin-outlet "user-card-metadata"}}
</div>
{{/if}}
{{#if publicUserFields}}
<div class="public-user-fields">
{{#each publicUserFields as |uf|}}
{{#if uf.value}}
<div class="public-user-field {{uf.field.dasherized_name}}">
<span class="user-field-name">{{uf.field.name}}:</span>
<span class="user-field-value">{{uf.value}}</span>
</div>
{{/if}}
{{/each}}
</div>
{{/if}}
{{#if showBadges}}
<div class="badge-section">
{{#each user.featured_user_badges as |ub|}}
{{user-badge badge=ub.badge user=user}}
{{/each}}
{{#if showMoreBadges}}
{{#link-to 'user.badges' user class="btn more-user-badges"}}
{{i18n 'badges.more_badges' count=moreBadgesCount}}
{{/link-to}}
{{/if}}
</div>
{{/if}}
</div>
{{/if}}
{{user-card-contents
currentPath=application.currentPath
topic=topic.model
showUser="showUser"
togglePosts="togglePosts"
composePrivateMessage="composePrivateMessage"
deleteUser="deleteUser"}}

View File

@ -1,149 +0,0 @@
import { wantsNewWindow } from 'discourse/lib/intercept-click';
import { setting } from 'discourse/lib/computed';
import CleansUp from 'discourse/mixins/cleans-up';
import afterTransition from 'discourse/lib/after-transition';
const clickOutsideEventName = "mousedown.outside-user-card",
clickDataExpand = "click.discourse-user-card",
clickMention = "click.discourse-user-mention";
export default Ember.View.extend(CleansUp, {
elementId: 'user-card',
classNameBindings: ['controller.visible:show', 'controller.showBadges', 'controller.hasCardBadgeImage'],
allowBackgrounds: setting('allow_profile_backgrounds'),
addBackground: function() {
const url = this.get('controller.user.card_background');
if (!this.get('allowBackgrounds')) { return; }
const $this = this.$();
if (!$this) { return; }
if (Ember.isEmpty(url)) {
$this.css('background-image', '').addClass('no-bg');
} else {
$this.css('background-image', "url(" + Discourse.getURLWithCDN(url) + ")").removeClass('no-bg');
}
}.observes('controller.user.card_background'),
_setup: function() {
afterTransition(this.$(), this._hide.bind(this));
$('html').off(clickOutsideEventName)
.on(clickOutsideEventName, (e) => {
if (this.get('controller.visible')) {
const $target = $(e.target);
if ($target.closest('[data-user-card]').data('userCard') ||
$target.closest('a.mention').length > 0 ||
$target.closest('#user-card').length > 0) {
return;
}
this.get('controller').close();
}
return true;
});
const expand = (username, $target) => {
const postId = $target.parents('article').data('post-id'),
user = this.get('controller').show(username, postId, $target[0]);
if (user !== undefined) {
user.then( () => this._willShow($target) ).catch( () => this._hide() );
} else {
this._hide();
}
return false;
};
$('#main-outlet').on(clickDataExpand, '[data-user-card]', (e) => {
if (wantsNewWindow(e)) { return; }
const $target = $(e.currentTarget),
username = $target.data('user-card');
return expand(username, $target);
});
$('#main-outlet').on(clickMention, 'a.mention', (e) => {
if (wantsNewWindow(e)) { return; }
const $target = $(e.target),
username = $target.text().replace(/^@/, '');
return expand(username, $target);
});
this.appEvents.on('usercard:shown', this, '_shown');
}.on('didInsertElement'),
_shown() {
// After the card is shown, focus on the first link
//
// note: we DO NOT use afterRender here cause _willShow may
// run after _shown, if we allowed this to happen the usercard
// may be offscreen and we may scroll all the way to it on focus
Ember.run.next(null, () => this.$('a:first').focus() );
},
_willShow(target) {
const rtl = ($('html').css('direction')) === 'rtl';
if (!target) { return; }
const width = this.$().width();
Ember.run.schedule('afterRender', () => {
if (target) {
let position = target.offset();
if (position) {
if (rtl) { // The site direction is rtl
position.right = $(window).width() - position.left + 10;
position.left = 'auto';
let overage = ($(window).width() - 50) - (position.right + width);
if (overage < 0) {
position.right += overage;
position.top += target.height() + 48;
}
} else { // The site direction is ltr
position.left += target.width() + 10;
let overage = ($(window).width() - 50) - (position.left + width);
if (overage < 0) {
position.left += overage;
position.top += target.height() + 48;
}
}
position.top -= $('#main-outlet').offset().top;
this.$().css(position);
}
this.appEvents.trigger('usercard:shown');
}
});
},
_hide() {
if (!this.get('controller.visible')) {
this.$().css({left: -9999, top: -9999});
}
},
cleanUp() {
this.get('controller').close();
},
keyUp(e) {
if (e.keyCode === 27) { // ESC
const target = this.get('controller.cardTarget');
this.cleanUp();
target.focus();
}
},
_removeEvents: function() {
$('html').off(clickOutsideEventName);
$('#main').off(clickDataExpand).off(clickMention);
this.appEvents.off('usercard:shown', this, '_shown');
}.on('willDestroyElement')
});