Clean up JS, add YUIDoc support, automatically create IIFE via asset pipeline

This commit is contained in:
Robin Ward 2013-02-22 15:41:12 -05:00
parent 0321643636
commit e461c84253
217 changed files with 11996 additions and 12131 deletions

View File

@ -1,63 +1,59 @@
(function() { /**
This controller supports interface for creating custom CSS skins in Discourse.
@class AdminCustomizeController
@extends Ember.Controller
@namespace Discourse
@module Discourse
**/
Discourse.AdminCustomizeController = Ember.Controller.extend({
/** /**
This controller supports interface for creating custom CSS skins in Discourse. Create a new customization style
@class AdminCustomizeController @method newCustomization
@extends Ember.Controller **/
@namespace Discourse newCustomization: function() {
@module Discourse var item = Discourse.SiteCustomization.create({name: 'New Style'});
**/ this.get('content').pushObject(item);
window.Discourse.AdminCustomizeController = Ember.Controller.extend({ this.set('content.selectedItem', item);
},
/** /**
Create a new customization style Select a given style
@method newCustomization @method selectStyle
**/ @param {Discourse.SiteCustomization} style The style we are selecting
newCustomization: function() { **/
var item = Discourse.SiteCustomization.create({name: 'New Style'}); selectStyle: function(style) {
this.get('content').pushObject(item); this.set('content.selectedItem', style);
this.set('content.selectedItem', item); },
},
/** /**
Select a given style Save the current customization
@method selectStyle @method save
@param {Discourse.SiteCustomization} style The style we are selecting **/
**/ save: function() {
selectStyle: function(style) { this.get('content.selectedItem').save();
this.set('content.selectedItem', style); },
},
/** /**
Save the current customization Destroy the current customization
@method save @method destroy
**/ **/
save: function() { destroy: function() {
this.get('content.selectedItem').save(); var _this = this;
}, return bootbox.confirm(Em.String.i18n("admin.customize.delete_confirm"), Em.String.i18n("no_value"), Em.String.i18n("yes_value"), function(result) {
var selected;
if (result) {
selected = _this.get('content.selectedItem');
selected["delete"]();
_this.set('content.selectedItem', null);
return _this.get('content').removeObject(selected);
}
});
}
/** });
Destroy the current customization
@method destroy
**/
destroy: function() {
var _this = this;
return bootbox.confirm(Em.String.i18n("admin.customize.delete_confirm"), Em.String.i18n("no_value"), Em.String.i18n("yes_value"), function(result) {
var selected;
if (result) {
selected = _this.get('content.selectedItem');
selected["delete"]();
_this.set('content.selectedItem', null);
return _this.get('content').removeObject(selected);
}
});
}
});
}).call(this);

View File

@ -1,16 +1,12 @@
(function() { /**
This controller supports the default interface when you enter the admin section.
/** @class AdminDashboardController
This controller supports the default interface when you enter the admin section. @extends Ember.Controller
@namespace Discourse
@class AdminDashboardController @module Discourse
@extends Ember.Controller **/
@namespace Discourse Discourse.AdminDashboardController = Ember.Controller.extend({
@module Discourse loading: true,
**/ versionCheck: null
window.Discourse.AdminDashboardController = Ember.Controller.extend({ });
loading: true,
versionCheck: null
});
}).call(this);

View File

@ -1,43 +1,39 @@
(function() { /**
This controller supports the interface for reviewing email logs.
@class AdminEmailLogsController
@extends Ember.ArrayController
@namespace Discourse
@module Discourse
**/
Discourse.AdminEmailLogsController = Ember.ArrayController.extend(Discourse.Presence, {
/**
Is the "send test email" button disabled?
@property sendTestEmailDisabled
**/
sendTestEmailDisabled: (function() {
return this.blank('testEmailAddress');
}).property('testEmailAddress'),
/** /**
This controller supports the interface for reviewing email logs. Sends a test email to the currently entered email address
@class AdminEmailLogsController @method sendTestEmail
@extends Ember.ArrayController **/
@namespace Discourse sendTestEmail: function() {
@module Discourse var _this = this;
**/ _this.set('sentTestEmail', false);
window.Discourse.AdminEmailLogsController = Ember.ArrayController.extend(Discourse.Presence, { jQuery.ajax({
url: '/admin/email_logs/test',
/** type: 'POST',
Is the "send test email" button disabled? data: { email_address: this.get('testEmailAddress') },
success: function() {
@property sendTestEmailDisabled return _this.set('sentTestEmail', true);
**/ }
sendTestEmailDisabled: (function() { });
return this.blank('testEmailAddress'); return false;
}).property('testEmailAddress'), }
/** });
Sends a test email to the currently entered email address
@method sendTestEmail
**/
sendTestEmail: function() {
var _this = this;
_this.set('sentTestEmail', false);
jQuery.ajax({
url: '/admin/email_logs/test',
type: 'POST',
data: { email_address: this.get('testEmailAddress') },
success: function() {
return _this.set('sentTestEmail', true);
}
});
return false;
}
});
}).call(this);

View File

@ -1,63 +1,59 @@
(function() { /**
This controller supports the interface for dealing with flags in the admin section.
@class AdminFlagsController
@extends Ember.Controller
@namespace Discourse
@module Discourse
**/
Discourse.AdminFlagsController = Ember.Controller.extend({
/**
Clear all flags on a post
@method clearFlags
@param {Discourse.FlaggedPost} item The post whose flags we want to clear
**/
clearFlags: function(item) {
var _this = this;
item.clearFlags().then((function() {
_this.content.removeObject(item);
}), (function() {
bootbox.alert("something went wrong");
}));
},
/** /**
This controller supports the interface for dealing with flags in the admin section. Deletes a post
@class AdminFlagsController @method deletePost
@extends Ember.Controller @param {Discourse.FlaggedPost} item The post to delete
@namespace Discourse **/
@module Discourse deletePost: function(item) {
**/ var _this = this;
window.Discourse.AdminFlagsController = Ember.Controller.extend({ item.deletePost().then((function() {
_this.content.removeObject(item);
/** }), (function() {
Clear all flags on a post bootbox.alert("something went wrong");
}));
},
@method clearFlags /**
@param {Discourse.FlaggedPost} item The post whose flags we want to clear Are we viewing the 'old' view?
**/
clearFlags: function(item) {
var _this = this;
item.clearFlags().then((function() {
_this.content.removeObject(item);
}), (function() {
bootbox.alert("something went wrong");
}));
},
/** @property adminOldFlagsView
Deletes a post **/
adminOldFlagsView: (function() {
return this.query === 'old';
}).property('query'),
@method deletePost /**
@param {Discourse.FlaggedPost} item The post to delete Are we viewing the 'active' view?
**/
deletePost: function(item) {
var _this = this;
item.deletePost().then((function() {
_this.content.removeObject(item);
}), (function() {
bootbox.alert("something went wrong");
}));
},
/** @property adminActiveFlagsView
Are we viewing the 'old' view? **/
adminActiveFlagsView: (function() {
@property adminOldFlagsView return this.query === 'active';
**/ }).property('query')
adminOldFlagsView: (function() {
return this.query === 'old'; });
}).property('query'),
/**
Are we viewing the 'active' view?
@property adminActiveFlagsView
**/
adminActiveFlagsView: (function() {
return this.query === 'active';
}).property('query')
});
}).call(this);

View File

@ -1,74 +1,70 @@
(function() { /**
This controller supports the interface for SiteSettings.
@class AdminSiteSettingsController
@extends Ember.ArrayController
@namespace Discourse
@module Discourse
**/
Discourse.AdminSiteSettingsController = Ember.ArrayController.extend(Discourse.Presence, {
filter: null,
onlyOverridden: false,
/** /**
This controller supports the interface for SiteSettings. The list of settings based on the current filters
@class AdminSiteSettingsController @property filteredContent
@extends Ember.ArrayController **/
@namespace Discourse filteredContent: (function() {
@module Discourse var filter,
**/ _this = this;
window.Discourse.AdminSiteSettingsController = Ember.ArrayController.extend(Discourse.Presence, { if (!this.present('content')) return null;
filter: null, if (this.get('filter')) {
onlyOverridden: false, filter = this.get('filter').toLowerCase();
}
/** return this.get('content').filter(function(item, index, enumerable) {
The list of settings based on the current filters if (_this.get('onlyOverridden') && !item.get('overridden')) return false;
if (filter) {
@property filteredContent if (item.get('setting').toLowerCase().indexOf(filter) > -1) return true;
**/ if (item.get('description').toLowerCase().indexOf(filter) > -1) return true;
filteredContent: (function() { if (item.get('value').toLowerCase().indexOf(filter) > -1) return true;
var filter, return false;
_this = this;
if (!this.present('content')) return null;
if (this.get('filter')) {
filter = this.get('filter').toLowerCase();
} }
return this.get('content').filter(function(item, index, enumerable) { return true;
if (_this.get('onlyOverridden') && !item.get('overridden')) return false; });
if (filter) { }).property('filter', 'content.@each', 'onlyOverridden'),
if (item.get('setting').toLowerCase().indexOf(filter) > -1) return true;
if (item.get('description').toLowerCase().indexOf(filter) > -1) return true;
if (item.get('value').toLowerCase().indexOf(filter) > -1) return true;
return false;
}
return true; /**
}); Reset a setting to its default value
}).property('filter', 'content.@each', 'onlyOverridden'),
/** @method resetDefault
Reset a setting to its default value @param {Discourse.SiteSetting} setting The setting we want to revert
**/
resetDefault: function(setting) {
setting.set('value', setting.get('default'));
setting.save();
},
@method resetDefault /**
@param {Discourse.SiteSetting} setting The setting we want to revert Save changes to a site setting
**/
resetDefault: function(setting) {
setting.set('value', setting.get('default'));
setting.save();
},
/** @method save
Save changes to a site setting @param {Discourse.SiteSetting} setting The setting we've changed
**/
save: function(setting) {
setting.save();
},
@method save /**
@param {Discourse.SiteSetting} setting The setting we've changed Cancel changes to a site setting
**/
save: function(setting) {
setting.save();
},
/** @method cancel
Cancel changes to a site setting @param {Discourse.SiteSetting} setting The setting we've changed but want to revert
**/
@method cancel cancel: function(setting) {
@param {Discourse.SiteSetting} setting The setting we've changed but want to revert setting.resetValue();
**/ }
cancel: function(setting) {
setting.resetValue(); });
}
});
}).call(this);

View File

@ -1,111 +1,107 @@
(function() { /**
This controller supports the interface for listing users in the admin section.
@class AdminUsersListController
@extends Ember.ArrayController
@namespace Discourse
@module Discourse
**/
Discourse.AdminUsersListController = Ember.ArrayController.extend(Discourse.Presence, {
username: null,
query: null,
selectAll: false,
content: null,
/** /**
This controller supports the interface for listing users in the admin section. Triggered when the selectAll property is changed
@class AdminUsersListController @event selectAll
@extends Ember.ArrayController **/
@namespace Discourse selectAllChanged: (function() {
@module Discourse var _this = this;
**/ this.get('content').each(function(user) {
window.Discourse.AdminUsersListController = Ember.ArrayController.extend(Discourse.Presence, { user.set('selected', _this.get('selectAll'));
username: null, });
query: null, }).observes('selectAll'),
selectAll: false,
content: null,
/** /**
Triggered when the selectAll property is changed Triggered when the username filter is changed
@event selectAll @event filterUsers
**/ **/
selectAllChanged: (function() { filterUsers: Discourse.debounce(function() {
var _this = this; this.refreshUsers();
this.get('content').each(function(user) { }, 250).observes('username'),
user.set('selected', _this.get('selectAll'));
});
}).observes('selectAll'),
/** /**
Triggered when the username filter is changed Triggered when the order of the users list is changed
@event filterUsers @event orderChanged
**/ **/
filterUsers: Discourse.debounce(function() { orderChanged: (function() {
this.refreshUsers();
}).observes('query'),
/**
Do we want to show the approval controls?
@property showApproval
**/
showApproval: (function() {
if (!Discourse.SiteSettings.must_approve_users) return false;
if (this.get('query') === 'new') return true;
if (this.get('query') === 'pending') return true;
}).property('query'),
/**
How many users are currently selected
@property selectedCount
**/
selectedCount: (function() {
if (this.blank('content')) return 0;
return this.get('content').filterProperty('selected').length;
}).property('content.@each.selected'),
/**
Do we have any selected users?
@property hasSelection
**/
hasSelection: (function() {
return this.get('selectedCount') > 0;
}).property('selectedCount'),
/**
Refresh the current list of users.
@method refreshUsers
**/
refreshUsers: function() {
this.set('content', Discourse.AdminUser.findAll(this.get('query'), this.get('username')));
},
/**
Show the list of users.
@method show
**/
show: function(term) {
if (this.get('query') === term) {
this.refreshUsers(); this.refreshUsers();
}, 250).observes('username'), return;
/**
Triggered when the order of the users list is changed
@event orderChanged
**/
orderChanged: (function() {
this.refreshUsers();
}).observes('query'),
/**
Do we want to show the approval controls?
@property showApproval
**/
showApproval: (function() {
if (!Discourse.SiteSettings.must_approve_users) return false;
if (this.get('query') === 'new') return true;
if (this.get('query') === 'pending') return true;
}).property('query'),
/**
How many users are currently selected
@property selectedCount
**/
selectedCount: (function() {
if (this.blank('content')) return 0;
return this.get('content').filterProperty('selected').length;
}).property('content.@each.selected'),
/**
Do we have any selected users?
@property hasSelection
**/
hasSelection: (function() {
return this.get('selectedCount') > 0;
}).property('selectedCount'),
/**
Refresh the current list of users.
@method refreshUsers
**/
refreshUsers: function() {
this.set('content', Discourse.AdminUser.findAll(this.get('query'), this.get('username')));
},
/**
Show the list of users.
@method show
**/
show: function(term) {
if (this.get('query') === term) {
this.refreshUsers();
return;
}
this.set('query', term);
},
/**
Approve all the currently selected users.
@method approveUsers
**/
approveUsers: function() {
Discourse.AdminUser.bulkApprove(this.get('content').filterProperty('selected'));
} }
this.set('query', term);
}); },
}).call(this); /**
Approve all the currently selected users.
@method approveUsers
**/
approveUsers: function() {
Discourse.AdminUser.bulkApprove(this.get('content').filterProperty('selected'));
}
});

View File

@ -1,190 +1,186 @@
(function() { /**
Our data model for dealing with users from the admin section.
/** @class AdminUser
Our data model for dealing with users from the admin section. @extends Discourse.Model
@namespace Discourse
@module Discourse
**/
Discourse.AdminUser = Discourse.Model.extend({
deleteAllPosts: function() {
this.set('can_delete_all_posts', false);
jQuery.ajax("/admin/users/" + (this.get('id')) + "/delete_all_posts", {type: 'PUT'});
},
@class AdminUser // Revoke the user's admin access
@extends Discourse.Model revokeAdmin: function() {
@namespace Discourse this.set('admin', false);
@module Discourse this.set('can_grant_admin', true);
**/ this.set('can_revoke_admin', false);
window.Discourse.AdminUser = Discourse.Model.extend({ return jQuery.ajax("/admin/users/" + (this.get('id')) + "/revoke_admin", {type: 'PUT'});
},
deleteAllPosts: function() {
this.set('can_delete_all_posts', false);
jQuery.ajax("/admin/users/" + (this.get('id')) + "/delete_all_posts", {type: 'PUT'});
},
// Revoke the user's admin access grantAdmin: function() {
revokeAdmin: function() { this.set('admin', true);
this.set('admin', false); this.set('can_grant_admin', false);
this.set('can_grant_admin', true); this.set('can_revoke_admin', true);
this.set('can_revoke_admin', false); jQuery.ajax("/admin/users/" + (this.get('id')) + "/grant_admin", {type: 'PUT'});
return jQuery.ajax("/admin/users/" + (this.get('id')) + "/revoke_admin", {type: 'PUT'}); },
},
grantAdmin: function() { // Revoke the user's moderation access
this.set('admin', true); revokeModeration: function() {
this.set('can_grant_admin', false); this.set('moderator', false);
this.set('can_revoke_admin', true); this.set('can_grant_moderation', true);
jQuery.ajax("/admin/users/" + (this.get('id')) + "/grant_admin", {type: 'PUT'}); this.set('can_revoke_moderation', false);
}, return jQuery.ajax("/admin/users/" + (this.get('id')) + "/revoke_moderation", {type: 'PUT'});
},
// Revoke the user's moderation access grantModeration: function() {
revokeModeration: function() { this.set('moderator', true);
this.set('moderator', false); this.set('can_grant_moderation', false);
this.set('can_grant_moderation', true); this.set('can_revoke_moderation', true);
this.set('can_revoke_moderation', false); jQuery.ajax("/admin/users/" + (this.get('id')) + "/grant_moderation", {type: 'PUT'});
return jQuery.ajax("/admin/users/" + (this.get('id')) + "/revoke_moderation", {type: 'PUT'}); },
},
grantModeration: function() { refreshBrowsers: function() {
this.set('moderator', true); jQuery.ajax("/admin/users/" + (this.get('id')) + "/refresh_browsers", {type: 'POST'});
this.set('can_grant_moderation', false); bootbox.alert("Message sent to all clients!");
this.set('can_revoke_moderation', true); },
jQuery.ajax("/admin/users/" + (this.get('id')) + "/grant_moderation", {type: 'PUT'});
},
refreshBrowsers: function() { approve: function() {
jQuery.ajax("/admin/users/" + (this.get('id')) + "/refresh_browsers", {type: 'POST'}); this.set('can_approve', false);
bootbox.alert("Message sent to all clients!"); this.set('approved', true);
}, this.set('approved_by', Discourse.get('currentUser'));
jQuery.ajax("/admin/users/" + (this.get('id')) + "/approve", {type: 'PUT'});
},
approve: function() { username_lower: (function() {
this.set('can_approve', false); return this.get('username').toLowerCase();
this.set('approved', true); }).property('username'),
this.set('approved_by', Discourse.get('currentUser'));
jQuery.ajax("/admin/users/" + (this.get('id')) + "/approve", {type: 'PUT'});
},
username_lower: (function() { trustLevel: (function() {
return this.get('username').toLowerCase(); return Discourse.get('site.trust_levels').findProperty('id', this.get('trust_level'));
}).property('username'), }).property('trust_level'),
trustLevel: (function() { canBan: (function() {
return Discourse.get('site.trust_levels').findProperty('id', this.get('trust_level')); return !this.admin && !this.moderator;
}).property('trust_level'), }).property('admin', 'moderator'),
canBan: (function() { banDuration: (function() {
return !this.admin && !this.moderator; var banned_at, banned_till;
}).property('admin', 'moderator'), banned_at = Date.create(this.banned_at);
banned_till = Date.create(this.banned_till);
return "" + (banned_at.short()) + " - " + (banned_till.short());
}).property('banned_till', 'banned_at'),
banDuration: (function() { ban: function() {
var banned_at, banned_till; var duration,
banned_at = Date.create(this.banned_at); _this = this;
banned_till = Date.create(this.banned_till); if (duration = parseInt(window.prompt(Em.String.i18n('admin.user.ban_duration')), 10)) {
return "" + (banned_at.short()) + " - " + (banned_till.short()); if (duration > 0) {
}).property('banned_till', 'banned_at'), return jQuery.ajax("/admin/users/" + this.id + "/ban", {
type: 'PUT',
data: {duration: duration},
success: function() {
window.location.reload();
},
error: function(e) {
var error;
error = Em.String.i18n('admin.user.ban_failed', {
error: "http: " + e.status + " - " + e.body
});
bootbox.alert(error);
}
});
}
}
},
ban: function() { unban: function() {
var duration, var _this = this;
_this = this; return jQuery.ajax("/admin/users/" + this.id + "/unban", {
if (duration = parseInt(window.prompt(Em.String.i18n('admin.user.ban_duration')), 10)) { type: 'PUT',
if (duration > 0) { success: function() {
return jQuery.ajax("/admin/users/" + this.id + "/ban", { window.location.reload();
type: 'PUT', },
data: {duration: duration}, error: function(e) {
success: function() { var error;
window.location.reload(); error = Em.String.i18n('admin.user.unban_failed', {
}, error: "http: " + e.status + " - " + e.body
error: function(e) { });
var error; bootbox.alert(error);
error = Em.String.i18n('admin.user.ban_failed', { }
error: "http: " + e.status + " - " + e.body });
}); },
bootbox.alert(error);
} impersonate: function() {
}); var _this = this;
return jQuery.ajax("/admin/impersonate", {
type: 'POST',
data: {
username_or_email: this.get('username')
},
success: function() {
document.location = "/";
},
error: function(e) {
_this.set('loading', false);
if (e.status === 404) {
return bootbox.alert(Em.String.i18n('admin.impersonate.not_found'));
} else {
return bootbox.alert(Em.String.i18n('admin.impersonate.invalid'));
} }
} }
}, });
}
unban: function() { });
var _this = this;
return jQuery.ajax("/admin/users/" + this.id + "/unban", {
type: 'PUT',
success: function() {
window.location.reload();
},
error: function(e) {
var error;
error = Em.String.i18n('admin.user.unban_failed', {
error: "http: " + e.status + " - " + e.body
});
bootbox.alert(error);
}
});
},
impersonate: function() { window.Discourse.AdminUser.reopenClass({
var _this = this;
return jQuery.ajax("/admin/impersonate", {
type: 'POST',
data: {
username_or_email: this.get('username')
},
success: function() {
document.location = "/";
},
error: function(e) {
_this.set('loading', false);
if (e.status === 404) {
return bootbox.alert(Em.String.i18n('admin.impersonate.not_found'));
} else {
return bootbox.alert(Em.String.i18n('admin.impersonate.invalid'));
}
}
});
}
}); bulkApprove: function(users) {
users.each(function(user) {
user.set('approved', true);
user.set('can_approve', false);
return user.set('selected', false);
});
return jQuery.ajax("/admin/users/approve-bulk", {
type: 'PUT',
data: {
users: users.map(function(u) {
return u.id;
})
}
});
},
window.Discourse.AdminUser.reopenClass({ find: function(username) {
var promise;
promise = new RSVP.Promise();
jQuery.ajax({
url: "/admin/users/" + username,
success: function(result) {
return promise.resolve(Discourse.AdminUser.create(result));
}
});
return promise;
},
bulkApprove: function(users) { findAll: function(query, filter) {
users.each(function(user) { var result;
user.set('approved', true); result = Em.A();
user.set('can_approve', false); jQuery.ajax({
return user.set('selected', false); url: "/admin/users/list/" + query + ".json",
}); data: {
return jQuery.ajax("/admin/users/approve-bulk", { filter: filter
type: 'PUT', },
data: { success: function(users) {
users: users.map(function(u) { return users.each(function(u) {
return u.id; return result.pushObject(Discourse.AdminUser.create(u));
}) });
} }
}); });
}, return result;
}
find: function(username) { });
var promise;
promise = new RSVP.Promise();
jQuery.ajax({
url: "/admin/users/" + username,
success: function(result) {
return promise.resolve(Discourse.AdminUser.create(result));
}
});
return promise;
},
findAll: function(query, filter) {
var result;
result = Em.A();
jQuery.ajax({
url: "/admin/users/list/" + query + ".json",
data: {
filter: filter
},
success: function(users) {
return users.each(function(u) {
return result.pushObject(Discourse.AdminUser.create(u));
});
}
});
return result;
}
});
}).call(this);

View File

@ -1,38 +1,36 @@
(function() { /**
Our data model for representing an email log.
/** @class EmailLog
Our data model for representing an email log. @extends Discourse.Model
@namespace Discourse
@module Discourse
**/
Discourse.EmailLog = Discourse.Model.extend({});
@class EmailLog Discourse.EmailLog.reopenClass({
@extends Discourse.Model
@namespace Discourse
@module Discourse
**/
window.Discourse.EmailLog = Discourse.Model.extend({});
window.Discourse.EmailLog.reopenClass({ create: function(attrs) {
if (attrs.user) {
create: function(attrs) { attrs.user = Discourse.AdminUser.create(attrs.user);
if (attrs.user) {
attrs.user = Discourse.AdminUser.create(attrs.user);
}
return this._super(attrs);
},
findAll: function(filter) {
var result;
result = Em.A();
jQuery.ajax({
url: "/admin/email_logs.json",
data: { filter: filter },
success: function(logs) {
logs.each(function(log) {
result.pushObject(Discourse.EmailLog.create(log));
});
}
});
return result;
} }
}); return this._super(attrs);
},
findAll: function(filter) {
var result;
result = Em.A();
jQuery.ajax({
url: "/admin/email_logs.json",
data: { filter: filter },
success: function(logs) {
logs.each(function(log) {
result.pushObject(Discourse.EmailLog.create(log));
});
}
});
return result;
}
});
}).call(this);

View File

@ -1,85 +1,56 @@
(function() { /**
Our data model for interacting with flagged posts.
/** @class FlaggedPost
Our data model for interacting with flagged posts. @extends Discourse.Post
@namespace Discourse
@module Discourse
**/
Discourse.FlaggedPost = Discourse.Post.extend({
@class FlaggedPost flaggers: (function() {
@extends Discourse.Post var r,
@namespace Discourse _this = this;
@module Discourse r = [];
**/ this.post_actions.each(function(a) {
window.Discourse.FlaggedPost = Discourse.Post.extend({ return r.push(_this.userLookup[a.user_id]);
});
return r;
}).property(),
flaggers: (function() { messages: (function() {
var r, var r,
_this = this; _this = this;
r = []; r = [];
this.post_actions.each(function(a) { this.post_actions.each(function(a) {
return r.push(_this.userLookup[a.user_id]); if (a.message) {
}); return r.push({
return r; user: _this.userLookup[a.user_id],
}).property(), message: a.message
messages: (function() {
var r,
_this = this;
r = [];
this.post_actions.each(function(a) {
if (a.message) {
return r.push({
user: _this.userLookup[a.user_id],
message: a.message
});
}
});
return r;
}).property(),
lastFlagged: (function() {
return this.post_actions[0].created_at;
}).property(),
user: (function() {
return this.userLookup[this.user_id];
}).property(),
topicHidden: (function() {
return this.get('topic_visible') === 'f';
}).property('topic_hidden'),
deletePost: function() {
var promise;
promise = new RSVP.Promise();
if (this.get('post_number') === "1") {
return jQuery.ajax("/t/" + this.topic_id, {
type: 'DELETE',
cache: false,
success: function() {
promise.resolve();
},
error: function(e) {
promise.reject();
}
});
} else {
return jQuery.ajax("/posts/" + this.id, {
type: 'DELETE',
cache: false,
success: function() {
promise.resolve();
},
error: function(e) {
promise.reject();
}
}); });
} }
}, });
return r;
}).property(),
clearFlags: function() { lastFlagged: (function() {
var promise; return this.post_actions[0].created_at;
promise = new RSVP.Promise(); }).property(),
jQuery.ajax("/admin/flags/clear/" + this.id, {
type: 'POST', user: (function() {
return this.userLookup[this.user_id];
}).property(),
topicHidden: (function() {
return this.get('topic_visible') === 'f';
}).property('topic_hidden'),
deletePost: function() {
var promise;
promise = new RSVP.Promise();
if (this.get('post_number') === "1") {
return jQuery.ajax("/t/" + this.topic_id, {
type: 'DELETE',
cache: false, cache: false,
success: function() { success: function() {
promise.resolve(); promise.resolve();
@ -88,37 +59,64 @@
promise.reject(); promise.reject();
} }
}); });
return promise; } else {
}, return jQuery.ajax("/posts/" + this.id, {
type: 'DELETE',
hiddenClass: (function() { cache: false,
if (this.get('hidden') === "t") return "hidden-post"; success: function() {
}).property() promise.resolve();
},
}); error: function(e) {
promise.reject();
window.Discourse.FlaggedPost.reopenClass({
findAll: function(filter) {
var result;
result = Em.A();
jQuery.ajax({
url: "/admin/flags/" + filter + ".json",
success: function(data) {
var userLookup;
userLookup = {};
data.users.each(function(u) {
userLookup[u.id] = Discourse.User.create(u);
});
return data.posts.each(function(p) {
var f;
f = Discourse.FlaggedPost.create(p);
f.userLookup = userLookup;
return result.pushObject(f);
});
} }
}); });
return result;
} }
}); },
clearFlags: function() {
var promise;
promise = new RSVP.Promise();
jQuery.ajax("/admin/flags/clear/" + this.id, {
type: 'POST',
cache: false,
success: function() {
promise.resolve();
},
error: function(e) {
promise.reject();
}
});
return promise;
},
hiddenClass: (function() {
if (this.get('hidden') === "t") return "hidden-post";
}).property()
});
Discourse.FlaggedPost.reopenClass({
findAll: function(filter) {
var result;
result = Em.A();
jQuery.ajax({
url: "/admin/flags/" + filter + ".json",
success: function(data) {
var userLookup;
userLookup = {};
data.users.each(function(u) {
userLookup[u.id] = Discourse.User.create(u);
});
return data.posts.each(function(p) {
var f;
f = Discourse.FlaggedPost.create(p);
f.userLookup = userLookup;
return result.pushObject(f);
});
}
});
return result;
}
});
}).call(this);

View File

@ -1,117 +1,112 @@
(function() { /**
var SiteCustomizations; Our data model for interacting with site customizations.
/** @class SiteCustomization
Our data model for interacting with site customizations. @extends Discourse.Model
@namespace Discourse
@module Discourse
**/
Discourse.SiteCustomization = Discourse.Model.extend({
trackedProperties: ['enabled', 'name', 'stylesheet', 'header', 'override_default_style'],
@class SiteCustomization init: function() {
@extends Discourse.Model this._super();
@namespace Discourse return this.startTrackingChanges();
@module Discourse },
**/
window.Discourse.SiteCustomization = Discourse.Model.extend({ description: (function() {
trackedProperties: ['enabled', 'name', 'stylesheet', 'header', 'override_default_style'], return "" + this.name + (this.enabled ? ' (*)' : '');
}).property('selected', 'name'),
init: function() { changed: (function() {
this._super(); var _this = this;
return this.startTrackingChanges(); if (!this.originals) {
}, return false;
description: (function() {
return "" + this.name + (this.enabled ? ' (*)' : '');
}).property('selected', 'name'),
changed: (function() {
var _this = this;
if (!this.originals) {
return false;
}
return this.trackedProperties.any(function(p) {
return _this.originals[p] !== _this.get(p);
});
}).property('override_default_style', 'enabled', 'name', 'stylesheet', 'header', 'originals'),
startTrackingChanges: function() {
var _this = this;
this.set('originals', {});
return this.trackedProperties.each(function(p) {
_this.originals[p] = _this.get(p);
return true;
});
},
previewUrl: (function() {
return "/?preview-style=" + (this.get('key'));
}).property('key'),
disableSave: (function() {
return !this.get('changed');
}).property('changed'),
save: function() {
var data;
this.startTrackingChanges();
data = {
name: this.name,
enabled: this.enabled,
stylesheet: this.stylesheet,
header: this.header,
override_default_style: this.override_default_style
};
return jQuery.ajax({
url: "/admin/site_customizations" + (this.id ? '/' + this.id : ''),
data: {
site_customization: data
},
type: this.id ? 'PUT' : 'POST'
});
},
"delete": function() {
if (!this.id) return;
return jQuery.ajax({
url: "/admin/site_customizations/" + this.id,
type: 'DELETE'
});
} }
return this.trackedProperties.any(function(p) {
return _this.originals[p] !== _this.get(p);
});
}).property('override_default_style', 'enabled', 'name', 'stylesheet', 'header', 'originals'),
}); startTrackingChanges: function() {
var _this = this;
this.set('originals', {});
return this.trackedProperties.each(function(p) {
_this.originals[p] = _this.get(p);
return true;
});
},
SiteCustomizations = Ember.ArrayProxy.extend({ previewUrl: (function() {
selectedItemChanged: (function() { return "/?preview-style=" + (this.get('key'));
var selected; }).property('key'),
selected = this.get('selectedItem');
return this.get('content').each(function(i) {
return i.set('selected', selected === i);
});
}).observes('selectedItem')
});
Discourse.SiteCustomization.reopenClass({ disableSave: (function() {
findAll: function() { return !this.get('changed');
var content, }).property('changed'),
_this = this;
content = SiteCustomizations.create({ save: function() {
content: [], var data;
loading: true this.startTrackingChanges();
}); data = {
jQuery.ajax({ name: this.name,
url: "/admin/site_customizations", enabled: this.enabled,
dataType: "json", stylesheet: this.stylesheet,
success: function(data) { header: this.header,
if (data) { override_default_style: this.override_default_style
data.site_customizations.each(function(c) { };
var item; return jQuery.ajax({
item = Discourse.SiteCustomization.create(c); url: "/admin/site_customizations" + (this.id ? '/' + this.id : ''),
return content.pushObject(item); data: {
}); site_customization: data
} },
return content.set('loading', false); type: this.id ? 'PUT' : 'POST'
});
},
"delete": function() {
if (!this.id) return;
return jQuery.ajax({
url: "/admin/site_customizations/" + this.id,
type: 'DELETE'
});
}
});
var SiteCustomizations = Ember.ArrayProxy.extend({
selectedItemChanged: (function() {
var selected;
selected = this.get('selectedItem');
return this.get('content').each(function(i) {
return i.set('selected', selected === i);
});
}).observes('selectedItem')
});
Discourse.SiteCustomization.reopenClass({
findAll: function() {
var content,
_this = this;
content = SiteCustomizations.create({
content: [],
loading: true
});
jQuery.ajax({
url: "/admin/site_customizations",
dataType: "json",
success: function(data) {
if (data) {
data.site_customizations.each(function(c) {
var item;
item = Discourse.SiteCustomization.create(c);
return content.pushObject(item);
});
} }
}); return content.set('loading', false);
return content; }
} });
}); return content;
}
}).call(this); });

View File

@ -1,65 +1,63 @@
(function() { /**
Our data model for interacting with site settings.
/** @class SiteSetting
Our data model for interacting with site settings. @extends Discourse.Model
@namespace Discourse
@module Discourse
**/
Discourse.SiteSetting = Discourse.Model.extend({
// Whether a property is short.
short: (function() {
if (this.blank('value')) return true;
return this.get('value').toString().length < 80;
}).property('value'),
@class SiteSetting // Whether the site setting has changed
@extends Discourse.Model dirty: (function() {
@namespace Discourse return this.get('originalValue') !== this.get('value');
@module Discourse }).property('originalValue', 'value'),
**/
window.Discourse.SiteSetting = Discourse.Model.extend({
// Whether a property is short.
short: (function() {
if (this.blank('value')) return true;
return this.get('value').toString().length < 80;
}).property('value'),
// Whether the site setting has changed overridden: (function() {
dirty: (function() { var defaultVal, val;
return this.get('originalValue') !== this.get('value'); val = this.get('value');
}).property('originalValue', 'value'), defaultVal = this.get('default');
if (val && defaultVal) {
return val.toString() !== defaultVal.toString();
}
return val !== defaultVal;
}).property('value'),
overridden: (function() { resetValue: function() {
var defaultVal, val; this.set('value', this.get('originalValue'));
val = this.get('value'); },
defaultVal = this.get('default');
if (val && defaultVal) { save: function() {
return val.toString() !== defaultVal.toString(); // Update the setting
var _this = this;
return jQuery.ajax("/admin/site_settings/" + (this.get('setting')), {
data: { value: this.get('value') },
type: 'PUT',
success: function() {
_this.set('originalValue', _this.get('value'));
} }
return val !== defaultVal; });
}).property('value'), }
});
resetValue: function() { Discourse.SiteSetting.reopenClass({
this.set('value', this.get('originalValue')); findAll: function() {
}, var result;
result = Em.A();
save: function() { jQuery.get("/admin/site_settings", function(settings) {
// Update the setting return settings.each(function(s) {
var _this = this; s.originalValue = s.value;
return jQuery.ajax("/admin/site_settings/" + (this.get('setting')), { return result.pushObject(Discourse.SiteSetting.create(s));
data: { value: this.get('value') },
type: 'PUT',
success: function() {
_this.set('originalValue', _this.get('value'));
}
}); });
} });
}); return result;
}
});
window.Discourse.SiteSetting.reopenClass({
findAll: function() {
var result;
result = Em.A();
jQuery.get("/admin/site_settings", function(settings) {
return settings.each(function(s) {
s.originalValue = s.value;
return result.pushObject(Discourse.SiteSetting.create(s));
});
});
return result;
}
});
}).call(this);

View File

@ -1,39 +1,35 @@
(function() { /**
Our data model for determining whether there's a new version of Discourse
/** @class VersionCheck
Our data model for determining whether there's a new version of Discourse @extends Discourse.Model
@namespace Discourse
@module Discourse
**/
Discourse.VersionCheck = Discourse.Model.extend({
upToDate: function() {
return this.get('latest_version') === this.get('installed_version');
}.property('latest_version', 'installed_version'),
@class VersionCheck gitLink: function() {
@extends Discourse.Model return "https://github.com/discourse/discourse/tree/" + this.get('installed_sha');
@namespace Discourse }.property('installed_sha'),
@module Discourse
**/
window.Discourse.VersionCheck = Discourse.Model.extend({
upToDate: function() {
return this.get('latest_version') === this.get('installed_version');
}.property('latest_version', 'installed_version'),
gitLink: function() { shortSha: function() {
return "https://github.com/discourse/discourse/tree/" + this.get('installed_sha'); return this.get('installed_sha').substr(0,10);
}.property('installed_sha'), }.property('installed_sha')
});
shortSha: function() { Discourse.VersionCheck.reopenClass({
return this.get('installed_sha').substr(0,10); find: function() {
}.property('installed_sha') var promise = new RSVP.Promise();
}); jQuery.ajax({
url: '/admin/version_check',
Discourse.VersionCheck.reopenClass({ dataType: 'json',
find: function() { success: function(json) {
var promise = new RSVP.Promise(); promise.resolve(Discourse.VersionCheck.create(json));
jQuery.ajax({ }
url: '/admin/version_check', });
dataType: 'json', return promise;
success: function(json) { }
promise.resolve(Discourse.VersionCheck.create(json)); });
}
});
return promise;
}
});
}).call(this);

View File

@ -1,21 +1,19 @@
(function() { /**
Handles routes related to customization
/** @class AdminCustomizeRoute
Handles routes related to customization @extends Discourse.Route
@namespace Discourse
@module Discourse
**/
Discourse.AdminCustomizeRoute = Discourse.Route.extend({
model: function() {
return Discourse.SiteCustomization.findAll();
},
@class AdminCustomizeRoute renderTemplate: function() {
@extends Discourse.Route this.render({into: 'admin/templates/admin'});
@namespace Discourse }
@module Discourse });
**/
Discourse.AdminCustomizeRoute = Discourse.Route.extend({
model: function() {
return Discourse.SiteCustomization.findAll();
},
renderTemplate: function() {
this.render({into: 'admin/templates/admin'});
}
});
}).call(this);

View File

@ -1,33 +1,30 @@
(function() { /**
Handles the default admin route
/** @class AdminDashboardRoute
Handles the default admin route @extends Discourse.Route
@namespace Discourse
@class AdminDashboardRoute @module Discourse
@extends Discourse.Route **/
@namespace Discourse Discourse.AdminDashboardRoute = Discourse.Route.extend({
@module Discourse setupController: function(c) {
**/ if( !c.get('versionCheckedAt') || Date.create('12 hours ago') > c.get('versionCheckedAt') ) {
Discourse.AdminDashboardRoute = Discourse.Route.extend({ this.checkVersion(c);
setupController: function(c) {
if( !c.get('versionCheckedAt') || Date.create('12 hours ago') > c.get('versionCheckedAt') ) {
this.checkVersion(c);
}
},
renderTemplate: function() {
this.render({into: 'admin/templates/admin'});
},
checkVersion: function(c) {
if( Discourse.SiteSettings.version_checks ) {
Discourse.VersionCheck.find().then(function(vc) {
c.set('versionCheck', vc);
c.set('versionCheckedAt', new Date());
c.set('loading', false);
});
}
} }
}); },
renderTemplate: function() {
this.render({into: 'admin/templates/admin'});
},
checkVersion: function(c) {
if( Discourse.SiteSettings.version_checks ) {
Discourse.VersionCheck.find().then(function(vc) {
c.set('versionCheck', vc);
c.set('versionCheckedAt', new Date());
c.set('loading', false);
});
}
}
});
}).call(this);

View File

@ -1,21 +1,19 @@
(function() { /**
Handles routes related to viewing email logs.
/** @class AdminEmailLogsRoute
Handles routes related to viewing email logs. @extends Discourse.Route
@namespace Discourse
@module Discourse
**/
Discourse.AdminEmailLogsRoute = Discourse.Route.extend({
model: function() {
return Discourse.EmailLog.findAll();
},
@class AdminEmailLogsRoute renderTemplate: function() {
@extends Discourse.Route this.render('admin/templates/email_logs');
@namespace Discourse }
@module Discourse });
**/
Discourse.AdminEmailLogsRoute = Discourse.Route.extend({
model: function() {
return Discourse.EmailLog.findAll();
},
renderTemplate: function() {
this.render('admin/templates/email_logs');
}
});
}).call(this);

View File

@ -1,25 +1,23 @@
(function() { /**
Handles routes related to viewing active flags.
/** @class AdminFlagsActiveRoute
Handles routes related to viewing active flags. @extends Discourse.Route
@namespace Discourse
@module Discourse
**/
Discourse.AdminFlagsActiveRoute = Discourse.Route.extend({
@class AdminFlagsActiveRoute model: function() {
@extends Discourse.Route return Discourse.FlaggedPost.findAll('active');
@namespace Discourse },
@module Discourse
**/
Discourse.AdminFlagsActiveRoute = Discourse.Route.extend({
model: function() { setupController: function(controller, model) {
return Discourse.FlaggedPost.findAll('active'); var adminFlagsController = this.controllerFor('adminFlags');
}, adminFlagsController.set('content', model);
adminFlagsController.set('query', 'active');
}
setupController: function(controller, model) { });
var adminFlagsController = this.controllerFor('adminFlags');
adminFlagsController.set('content', model);
adminFlagsController.set('query', 'active');
}
});
}).call(this);

View File

@ -1,25 +1,23 @@
(function() { /**
Handles routes related to viewing old flags.
/** @class AdminFlagsOldRoute
Handles routes related to viewing old flags. @extends Discourse.Route
@namespace Discourse
@module Discourse
**/
Discourse.AdminFlagsOldRoute = Discourse.Route.extend({
model: function() {
return Discourse.FlaggedPost.findAll('old');
},
@class AdminFlagsOldRoute setupController: function(controller, model) {
@extends Discourse.Route var adminFlagsController = this.controllerFor('adminFlags');
@namespace Discourse adminFlagsController.set('content', model);
@module Discourse adminFlagsController.set('query', 'old');
**/ }
Discourse.AdminFlagsOldRoute = Discourse.Route.extend({
model: function() {
return Discourse.FlaggedPost.findAll('old');
},
setupController: function(controller, model) { });
var adminFlagsController = this.controllerFor('adminFlags');
adminFlagsController.set('content', model);
adminFlagsController.set('query', 'old');
}
});
}).call(this);

View File

@ -1,17 +1,15 @@
(function() { /**
Basic route for admin flags
/** @class AdminFlagsRoute
Basic route for admin flags @extends Discourse.Route
@namespace Discourse
@module Discourse
**/
Discourse.AdminFlagsRoute = Discourse.Route.extend({
renderTemplate: function() {
this.render('admin/templates/flags');
}
});
@class AdminFlagsRoute
@extends Discourse.Route
@namespace Discourse
@module Discourse
**/
Discourse.AdminFlagsRoute = Discourse.Route.extend({
renderTemplate: function() {
this.render('admin/templates/flags');
}
});
}).call(this);

View File

@ -1,17 +1,15 @@
(function() { /**
The base admin route
/** @class AdminRoute
The base admin route @extends Discourse.Route
@namespace Discourse
@module Discourse
**/
Discourse.AdminRoute = Discourse.Route.extend({
renderTemplate: function() {
this.render('admin/templates/admin');
}
});
@class AdminRoute
@extends Discourse.Route
@namespace Discourse
@module Discourse
**/
Discourse.AdminRoute = Discourse.Route.extend({
renderTemplate: function() {
this.render('admin/templates/admin');
}
});
}).call(this);

View File

@ -1,31 +1,32 @@
(function() { /**
Builds the routes for the admin section
/** @method buildRoutes
Declare all the routes used in the admin section. @for Discourse.AdminRoute
**/ **/
Discourse.buildRoutes(function() { Discourse.buildRoutes(function() {
return this.resource('admin', { path: '/admin' }, function() { this.resource('admin', { path: '/admin' }, function() {
this.route('dashboard', { path: '/' });
this.route('site_settings', { path: '/site_settings' });
this.route('email_logs', { path: '/email_logs' });
this.route('customize', { path: '/customize' });
this.resource('adminFlags', { path: '/flags' }, function() { this.route('dashboard', { path: '/' });
this.route('active', { path: '/active' }); this.route('site_settings', { path: '/site_settings' });
this.route('old', { path: '/old' }); this.route('email_logs', { path: '/email_logs' });
}); this.route('customize', { path: '/customize' });
this.resource('adminUsers', { path: '/users' }, function() {
this.resource('adminUser', { path: '/:username' });
this.resource('adminUsersList', { path: '/list' }, function() {
this.route('active', { path: '/active' });
this.route('new', { path: '/new' });
this.route('pending', { path: '/pending' });
});
});
this.resource('adminFlags', { path: '/flags' }, function() {
this.route('active', { path: '/active' });
this.route('old', { path: '/old' });
}); });
});
}).call(this); this.resource('adminUsers', { path: '/users' }, function() {
this.resource('adminUser', { path: '/:username' });
this.resource('adminUsersList', { path: '/list' }, function() {
this.route('active', { path: '/active' });
this.route('new', { path: '/new' });
this.route('pending', { path: '/pending' });
});
});
});
});

View File

@ -1,21 +1,19 @@
(function() { /**
Handles routes related to viewing and editing site settings.
/** @class AdminSiteSettingsRoute
Handles routes related to viewing and editing site settings. @extends Discourse.Route
@namespace Discourse
@module Discourse
**/
Discourse.AdminSiteSettingsRoute = Discourse.Route.extend({
model: function() {
return Discourse.SiteSetting.findAll();
},
@class AdminSiteSettingsRoute renderTemplate: function() {
@extends Discourse.Route this.render('admin/templates/site_settings', {into: 'admin/templates/admin'});
@namespace Discourse }
@module Discourse });
**/
Discourse.AdminSiteSettingsRoute = Discourse.Route.extend({
model: function() {
return Discourse.SiteSetting.findAll();
},
renderTemplate: function() {
this.render('admin/templates/site_settings', {into: 'admin/templates/admin'});
}
});
}).call(this);

View File

@ -1,22 +1,20 @@
(function() { /**
Handles routes related to users in the admin section.
/** @class AdminUserRoute
Handles routes related to users in the admin section. @extends Discourse.Route
@namespace Discourse
@module Discourse
**/
Discourse.AdminUserRoute = Discourse.Route.extend({
model: function(params) {
return Discourse.AdminUser.find(params.username);
},
@class AdminUserRoute renderTemplate: function() {
@extends Discourse.Route this.render('admin/templates/user', {into: 'admin/templates/admin'});
@namespace Discourse }
@module Discourse
**/
Discourse.AdminUserRoute = Discourse.Route.extend({
model: function(params) {
return Discourse.AdminUser.find(params.username);
},
renderTemplate: function() { });
this.render('admin/templates/user', {into: 'admin/templates/admin'});
}
});
}).call(this);

View File

@ -1,17 +1,15 @@
(function() { /**
Handles the route that lists active users.
/** @class AdminUsersListActiveRoute
Handles the route that lists active users. @extends Discourse.Route
@namespace Discourse
@module Discourse
**/
Discourse.AdminUsersListActiveRoute = Discourse.Route.extend({
setupController: function() {
return this.controllerFor('adminUsersList').show('active');
}
});
@class AdminUsersListActiveRoute
@extends Discourse.Route
@namespace Discourse
@module Discourse
**/
Discourse.AdminUsersListActiveRoute = Discourse.Route.extend({
setupController: function() {
return this.controllerFor('adminUsersList').show('active');
}
});
}).call(this);

View File

@ -1,17 +1,15 @@
(function() { /**
Handles the route that lists new users.
/** @class AdminUsersListNewRoute
Handles the route that lists new users. @extends Discourse.Route
@namespace Discourse
@module Discourse
**/
Discourse.AdminUsersListNewRoute = Discourse.Route.extend({
setupController: function() {
return this.controllerFor('adminUsersList').show('new');
}
});
@class AdminUsersListNewRoute
@extends Discourse.Route
@namespace Discourse
@module Discourse
**/
Discourse.AdminUsersListNewRoute = Discourse.Route.extend({
setupController: function() {
return this.controllerFor('adminUsersList').show('new');
}
});
}).call(this);

View File

@ -1,17 +1,15 @@
(function() { /**
Handles the route that lists pending users.
/** @class AdminUsersListNewRoute
Handles the route that lists pending users. @extends Discourse.Route
@namespace Discourse
@module Discourse
**/
Discourse.AdminUsersListPendingRoute = Discourse.Route.extend({
setupController: function() {
return this.controllerFor('adminUsersList').show('pending');
}
});
@class AdminUsersListNewRoute
@extends Discourse.Route
@namespace Discourse
@module Discourse
**/
Discourse.AdminUsersListPendingRoute = Discourse.Route.extend({
setupController: function() {
return this.controllerFor('adminUsersList').show('pending');
}
});
}).call(this);

View File

@ -1,17 +1,15 @@
(function() { /**
Handles the route that deals with listing users
/** @class AdminUsersListRoute
Handles the route that deals with listing users @extends Discourse.Route
@namespace Discourse
@module Discourse
**/
Discourse.AdminUsersListRoute = Discourse.Route.extend({
renderTemplate: function() {
this.render('admin/templates/users_list', {into: 'admin/templates/admin'});
}
});
@class AdminUsersListRoute
@extends Discourse.Route
@namespace Discourse
@module Discourse
**/
Discourse.AdminUsersListRoute = Discourse.Route.extend({
renderTemplate: function() {
this.render('admin/templates/users_list', {into: 'admin/templates/admin'});
}
});
}).call(this);

View File

@ -1,66 +1,65 @@
/*global ace:true */ /*global ace:true */
(function() {
/** /**
A view that wraps the ACE editor (http://ace.ajax.org/) A view that wraps the ACE editor (http://ace.ajax.org/)
@class AceEditorView @class AceEditorView
@extends Em.View @extends Em.View
@namespace Discourse @namespace Discourse
@module Discourse @module Discourse
**/ **/
Discourse.AceEditorView = Discourse.View.extend({ Discourse.AceEditorView = Discourse.View.extend({
mode: 'css', mode: 'css',
classNames: ['ace-wrapper'], classNames: ['ace-wrapper'],
contentChanged: (function() { contentChanged: (function() {
if (this.editor && !this.skipContentChangeEvent) { if (this.editor && !this.skipContentChangeEvent) {
return this.editor.getSession().setValue(this.get('content')); return this.editor.getSession().setValue(this.get('content'));
}
}).observes('content'),
render: function(buffer) {
buffer.push("<div class='ace'>");
if (this.get('content')) {
buffer.push(Handlebars.Utils.escapeExpression(this.get('content')));
}
return buffer.push("</div>");
},
willDestroyElement: function() {
if (this.editor) {
this.editor.destroy();
this.editor = null;
}
},
didInsertElement: function() {
var initAce,
_this = this;
initAce = function() {
_this.editor = ace.edit(_this.$('.ace')[0]);
_this.editor.setTheme("ace/theme/chrome");
_this.editor.setShowPrintMargin(false);
_this.editor.getSession().setMode("ace/mode/" + (_this.get('mode')));
return _this.editor.on("change", function(e) {
/* amending stuff as you type seems a bit out of scope for now - can revisit after launch
changes = @get('changes')
unless changes
changes = []
@set('changes', changes)
changes.push e.data
*/
_this.skipContentChangeEvent = true;
_this.set('content', _this.editor.getSession().getValue());
_this.skipContentChangeEvent = false;
});
};
if (window.ace) {
return initAce();
} else {
return $LAB.script('http://d1n0x3qji82z53.cloudfront.net/src-min-noconflict/ace.js').wait(initAce);
}
} }
}); }).observes('content'),
render: function(buffer) {
buffer.push("<div class='ace'>");
if (this.get('content')) {
buffer.push(Handlebars.Utils.escapeExpression(this.get('content')));
}
return buffer.push("</div>");
},
willDestroyElement: function() {
if (this.editor) {
this.editor.destroy();
this.editor = null;
}
},
didInsertElement: function() {
var initAce,
_this = this;
initAce = function() {
_this.editor = ace.edit(_this.$('.ace')[0]);
_this.editor.setTheme("ace/theme/chrome");
_this.editor.setShowPrintMargin(false);
_this.editor.getSession().setMode("ace/mode/" + (_this.get('mode')));
return _this.editor.on("change", function(e) {
/* amending stuff as you type seems a bit out of scope for now - can revisit after launch
changes = @get('changes')
unless changes
changes = []
@set('changes', changes)
changes.push e.data
*/
_this.skipContentChangeEvent = true;
_this.set('content', _this.editor.getSession().getValue());
_this.skipContentChangeEvent = false;
});
};
if (window.ace) {
return initAce();
} else {
return $LAB.script('http://d1n0x3qji82z53.cloudfront.net/src-min-noconflict/ace.js').wait(initAce);
}
}
});
}).call(this);

View File

@ -1,51 +1,50 @@
/*global Mousetrap:true */ /*global Mousetrap:true */
(function() {
/** /**
A view to handle site customizations A view to handle site customizations
@class AdminCustomizeView @class AdminCustomizeView
@extends Em.View @extends Em.View
@namespace Discourse @namespace Discourse
@module Discourse @module Discourse
**/ **/
Discourse.AdminCustomizeView = Discourse.View.extend({ Discourse.AdminCustomizeView = Discourse.View.extend({
templateName: 'admin/templates/customize', templateName: 'admin/templates/customize',
classNames: ['customize'], classNames: ['customize'],
init: function() { init: function() {
this._super(); this._super();
this.set('selected', 'stylesheet'); this.set('selected', 'stylesheet');
}, },
headerActive: (function() { headerActive: (function() {
return this.get('selected') === 'header'; return this.get('selected') === 'header';
}).property('selected'), }).property('selected'),
stylesheetActive: (function() { stylesheetActive: (function() {
return this.get('selected') === 'stylesheet'; return this.get('selected') === 'stylesheet';
}).property('selected'), }).property('selected'),
selectHeader: function() { selectHeader: function() {
this.set('selected', 'header'); this.set('selected', 'header');
}, },
selectStylesheet: function() { selectStylesheet: function() {
this.set('selected', 'stylesheet'); this.set('selected', 'stylesheet');
}, },
didInsertElement: function() { didInsertElement: function() {
var _this = this; var _this = this;
return Mousetrap.bindGlobal(['meta+s', 'ctrl+s'], function() { return Mousetrap.bindGlobal(['meta+s', 'ctrl+s'], function() {
_this.get('controller').save(); _this.get('controller').save();
return false; return false;
}); });
}, },
willDestroyElement: function() {
return Mousetrap.unbindGlobal('meta+s', 'ctrl+s');
}
});
willDestroyElement: function() {
return Mousetrap.unbindGlobal('meta+s', 'ctrl+s');
}
});
}).call(this);

View File

@ -1,33 +1,31 @@
(function() { /**
The default view in the admin section
/** @class AdminDashboardView
The default view in the admin section @extends Em.View
@namespace Discourse
@module Discourse
**/
Discourse.AdminDashboardView = Discourse.View.extend({
templateName: 'admin/templates/dashboard',
@class AdminDashboardView updateIconClasses: function() {
@extends Em.View var classes;
@namespace Discourse classes = "icon icon-warning-sign ";
@module Discourse if (this.get('controller.versionCheck.critical_updates')) {
**/ classes += "critical-updates-available";
Discourse.AdminDashboardView = Discourse.View.extend({ } else {
templateName: 'admin/templates/dashboard', classes += "updates-available";
}
return classes;
}.property('controller.versionCheck.critical_updates'),
updateIconClasses: function() { priorityClass: function() {
var classes; if (this.get('controller.versionCheck.critical_updates')) {
classes = "icon icon-warning-sign "; return 'version-check critical';
if (this.get('controller.versionCheck.critical_updates')) { }
classes += "critical-updates-available"; return 'version-check normal';
} else { }.property('controller.versionCheck.critical_updates')
classes += "updates-available"; });
}
return classes;
}.property('controller.versionCheck.critical_updates'),
priorityClass: function() {
if (this.get('controller.versionCheck.critical_updates')) {
return 'version-check critical';
}
return 'version-check normal';
}.property('controller.versionCheck.critical_updates')
});
}).call(this);

View File

@ -31,6 +31,7 @@
//= require ./discourse/views/view //= require ./discourse/views/view
//= require ./discourse/components/debounce //= require ./discourse/components/debounce
//= require ./discourse/controllers/controller //= require ./discourse/controllers/controller
//= require ./discourse/controllers/object_controller
//= require ./discourse/views/modal/modal_body_view //= require ./discourse/views/modal/modal_body_view
//= require ./discourse/models/model //= require ./discourse/models/model
//= require ./discourse/routes/discourse_route //= require ./discourse/routes/discourse_route

View File

@ -2,388 +2,387 @@
/*global assetPath:true*/ /*global assetPath:true*/
/*global FastClick:true*/ /*global FastClick:true*/
(function() { var csrf_token;
var csrf_token;
window.Discourse = Ember.Application.createWithMixins({ Discourse = Ember.Application.createWithMixins({
rootElement: '#main', rootElement: '#main',
// Data we want to remember for a short period // Data we want to remember for a short period
transient: Em.Object.create(), transient: Em.Object.create(),
hasFocus: true, hasFocus: true,
scrolling: false, scrolling: false,
// The highest seen post number by topic // The highest seen post number by topic
highestSeenByTopic: {}, highestSeenByTopic: {},
logoSmall: (function() { logoSmall: (function() {
var logo; var logo;
logo = Discourse.SiteSettings.logo_small_url; logo = Discourse.SiteSettings.logo_small_url;
if (logo && logo.length > 1) { if (logo && logo.length > 1) {
return "<img src='" + logo + "' width='33' height='33'>"; return "<img src='" + logo + "' width='33' height='33'>";
} else { } else {
return "<i class='icon-home'></i>"; return "<i class='icon-home'></i>";
}
}).property(),
titleChanged: (function() {
var title;
title = "";
if (this.get('title')) {
title += "" + (this.get('title')) + " - ";
}
title += Discourse.SiteSettings.title;
$('title').text(title);
if (!this.get('hasFocus') && this.get('notify')) {
title = "(*) " + title;
}
// chrome bug workaround see: http://stackoverflow.com/questions/2952384/changing-the-window-title-when-focussing-the-window-doesnt-work-in-chrome
window.setTimeout((function() {
document.title = ".";
document.title = title;
}), 200);
}).observes('title', 'hasFocus', 'notify'),
currentUserChanged: (function() {
var bus, user;
bus = Discourse.MessageBus;
// We don't want to receive any previous user notifications
bus.unsubscribe("/notification/*");
bus.callbackInterval = Discourse.SiteSettings.anon_polling_interval;
bus.enableLongPolling = false;
user = this.get('currentUser');
if (user) {
bus.callbackInterval = Discourse.SiteSettings.polling_interval;
bus.enableLongPolling = true;
if (user.admin) {
bus.subscribe("/flagged_counts", function(data) {
return user.set('site_flagged_posts_count', data.total);
});
} }
}).property(), return bus.subscribe("/notification/" + user.id, (function(data) {
user.set('unread_notifications', data.unread_notifications);
return user.set('unread_private_messages', data.unread_private_messages);
}), user.notification_channel_position);
}
}).observes('currentUser'),
notifyTitle: function() {
return this.set('notify', true);
},
titleChanged: (function() { // Browser aware replaceState
var title; replaceState: function(path) {
title = ""; if (window.history &&
if (this.get('title')) { window.history.pushState &&
title += "" + (this.get('title')) + " - "; window.history.replaceState &&
!navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]|WebApps\/.+CFNetwork)/)) {
if (window.location.pathname !== path) {
return history.replaceState({
path: path
}, null, path);
} }
title += Discourse.SiteSettings.title; }
jQuery('title').text(title); },
if (!this.get('hasFocus') && this.get('notify')) {
title = "(*) " + title;
}
// chrome bug workaround see: http://stackoverflow.com/questions/2952384/changing-the-window-title-when-focussing-the-window-doesnt-work-in-chrome
window.setTimeout((function() {
document.title = ".";
document.title = title;
}), 200);
}).observes('title', 'hasFocus', 'notify'),
currentUserChanged: (function() { openComposer: function(opts) {
var bus, user; // TODO, remove container link
bus = Discourse.MessageBus; var composer = Discourse.__container__.lookup('controller:composer');
if (composer) composer.open(opts);
},
// We don't want to receive any previous user notifications // Like router.route, but allow full urls rather than relative one
bus.unsubscribe("/notification/*"); // HERE BE HACKS - uses the ember container for now until we can do this nicer.
bus.callbackInterval = Discourse.SiteSettings.anon_polling_interval; routeTo: function(path) {
bus.enableLongPolling = false; var newMatches, newTopicId, oldMatches, oldTopicId, opts, router, topicController, topicRegexp;
user = this.get('currentUser'); path = path.replace(/https?\:\/\/[^\/]+/, '');
if (user) {
bus.callbackInterval = Discourse.SiteSettings.polling_interval;
bus.enableLongPolling = true;
if (user.admin) {
bus.subscribe("/flagged_counts", function(data) {
return user.set('site_flagged_posts_count', data.total);
});
}
return bus.subscribe("/notification/" + user.id, (function(data) {
user.set('unread_notifications', data.unread_notifications);
return user.set('unread_private_messages', data.unread_private_messages);
}), user.notification_channel_position);
}
}).observes('currentUser'),
notifyTitle: function() {
return this.set('notify', true);
},
// Browser aware replaceState // If we're in the same topic, don't push the state
replaceState: function(path) { topicRegexp = /\/t\/([^\/]+)\/(\d+)\/?(\d+)?/;
if (window.history && newMatches = topicRegexp.exec(path);
window.history.pushState && newTopicId = newMatches ? newMatches[2] : null;
window.history.replaceState && if (newTopicId) {
!navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]|WebApps\/.+CFNetwork)/)) { oldMatches = topicRegexp.exec(window.location.pathname);
if (window.location.pathname !== path) { if ((oldTopicId = oldMatches ? oldMatches[2] : void 0) && (oldTopicId === newTopicId)) {
return history.replaceState({ Discourse.replaceState(path);
path: path topicController = Discourse.__container__.lookup('controller:topic');
}, null, path); opts = {
trackVisit: false
};
if (newMatches[3]) {
opts.nearPost = newMatches[3];
} }
} topicController.get('content').loadPosts(opts);
},
openComposer: function(opts) {
// TODO, remove container link
var composer = Discourse.__container__.lookup('controller:composer');
if (composer) composer.open(opts);
},
// Like router.route, but allow full urls rather than relative one
// HERE BE HACKS - uses the ember container for now until we can do this nicer.
routeTo: function(path) {
var newMatches, newTopicId, oldMatches, oldTopicId, opts, router, topicController, topicRegexp;
path = path.replace(/https?\:\/\/[^\/]+/, '');
// If we're in the same topic, don't push the state
topicRegexp = /\/t\/([^\/]+)\/(\d+)\/?(\d+)?/;
newMatches = topicRegexp.exec(path);
newTopicId = newMatches ? newMatches[2] : null;
if (newTopicId) {
oldMatches = topicRegexp.exec(window.location.pathname);
if ((oldTopicId = oldMatches ? oldMatches[2] : void 0) && (oldTopicId === newTopicId)) {
Discourse.replaceState(path);
topicController = Discourse.__container__.lookup('controller:topic');
opts = {
trackVisit: false
};
if (newMatches[3]) {
opts.nearPost = newMatches[3];
}
topicController.get('content').loadPosts(opts);
return;
}
}
// Be wary of looking up the router. In this case, we have links in our
// HTML, say form compiled markdown posts, that need to be routed.
router = Discourse.__container__.lookup('router:main');
router.router.updateURL(path);
return router.handleURL(path);
},
// The classes of buttons to show on a post
postButtons: (function() {
return Discourse.SiteSettings.post_menu.split("|").map(function(i) {
return "" + (i.replace(/\+/, '').capitalize());
});
}).property('Discourse.SiteSettings.post_menu'),
bindDOMEvents: function() {
var $html, hasTouch,
_this = this;
$html = jQuery('html');
/* Add the discourse touch event */
hasTouch = false;
if ($html.hasClass('touch')) {
hasTouch = true;
}
if (Modernizr.prefixed("MaxTouchPoints", navigator) > 1) {
hasTouch = true;
}
if (hasTouch) {
$html.addClass('discourse-touch');
this.touch = true;
this.hasTouch = true;
$LAB.script(assetPath('defer/fastclick'))
.wait(function(){
// work around jshint hating side-effects
// its just the way the FastClick api is
var ignore = new FastClick(document.body);
});
} else {
$html.addClass('discourse-no-touch');
this.touch = false;
}
jQuery('#main').on('click.discourse', '[data-not-implemented=true]', function(e) {
e.preventDefault();
alert(Em.String.i18n('not_implemented'));
return false;
});
jQuery('#main').on('click.discourse', 'a', function(e) {
var $currentTarget, href;
if (e.isDefaultPrevented() || e.metaKey || e.ctrlKey) {
return;
}
$currentTarget = jQuery(e.currentTarget);
href = $currentTarget.attr('href');
if (href === void 0) {
return;
}
if (href === '#') {
return;
}
if ($currentTarget.attr('target')) {
return;
}
if ($currentTarget.data('auto-route')) {
return;
}
if ($currentTarget.hasClass('lightbox')) {
return;
}
if (href.indexOf("mailto:") === 0) {
return;
}
if (href.match(/^http[s]?:\/\//i) && !href.match(new RegExp("^http:\\/\\/" + window.location.hostname, "i"))) {
return;
}
e.preventDefault();
_this.routeTo(href);
return false;
});
return jQuery(window).focus(function() {
_this.set('hasFocus', true);
return _this.set('notify', false);
}).blur(function() {
return _this.set('hasFocus', false);
});
},
logout: function() {
var username,
_this = this;
username = this.get('currentUser.username');
Discourse.KeyValueStore.abandonLocal();
return jQuery.ajax("/session/" + username, {
type: 'DELETE',
success: function(result) {
/* To keep lots of our variables unbound, we can handle a redirect on logging out.
*/
return window.location.reload();
}
});
},
/* fancy probes in ember
*/
insertProbes: function() {
var topLevel;
if (typeof console === "undefined" || console === null) {
return; return;
} }
topLevel = function(fn, name) { }
return window.probes.measure(fn, { // Be wary of looking up the router. In this case, we have links in our
name: name, // HTML, say form compiled markdown posts, that need to be routed.
before: function(data, owner, args) { router = Discourse.__container__.lookup('router:main');
if (owner) { router.router.updateURL(path);
return window.probes.clear(); return router.handleURL(path);
} },
},
after: function(data, owner, args) { // The classes of buttons to show on a post
var ary, f, n, v, _ref; postButtons: (function() {
if (owner && data.time > 10) { return Discourse.SiteSettings.post_menu.split("|").map(function(i) {
f = function(name, data) { return "" + (i.replace(/\+/, '').capitalize());
if (data && data.count) { });
return "" + name + " - " + data.count + " calls " + ((data.time + 0.0).toFixed(2)) + "ms"; }).property('Discourse.SiteSettings.post_menu'),
}
}; bindDOMEvents: function() {
if (console && console.group) { var $html, hasTouch,
console.group(f(name, data)); _this = this;
} else { $html = $('html');
console.log("");
console.log(f(name, data)); /* Add the discourse touch event */
hasTouch = false;
if ($html.hasClass('touch')) {
hasTouch = true;
}
if (Modernizr.prefixed("MaxTouchPoints", navigator) > 1) {
hasTouch = true;
}
if (hasTouch) {
$html.addClass('discourse-touch');
this.touch = true;
this.hasTouch = true;
$LAB.script(assetPath('defer/fastclick'))
.wait(function(){
// work around jshint hating side-effects
// its just the way the FastClick api is
var ignore = new FastClick(document.body);
});
} else {
$html.addClass('discourse-no-touch');
this.touch = false;
}
$('#main').on('click.discourse', '[data-not-implemented=true]', function(e) {
e.preventDefault();
alert(Em.String.i18n('not_implemented'));
return false;
});
$('#main').on('click.discourse', 'a', function(e) {
var $currentTarget, href;
if (e.isDefaultPrevented() || e.metaKey || e.ctrlKey) {
return;
}
$currentTarget = $(e.currentTarget);
href = $currentTarget.attr('href');
if (href === void 0) {
return;
}
if (href === '#') {
return;
}
if ($currentTarget.attr('target')) {
return;
}
if ($currentTarget.data('auto-route')) {
return;
}
if ($currentTarget.hasClass('lightbox')) {
return;
}
if (href.indexOf("mailto:") === 0) {
return;
}
if (href.match(/^http[s]?:\/\//i) && !href.match(new RegExp("^http:\\/\\/" + window.location.hostname, "i"))) {
return;
}
e.preventDefault();
_this.routeTo(href);
return false;
});
return $(window).focus(function() {
_this.set('hasFocus', true);
return _this.set('notify', false);
}).blur(function() {
return _this.set('hasFocus', false);
});
},
logout: function() {
var username,
_this = this;
username = this.get('currentUser.username');
Discourse.KeyValueStore.abandonLocal();
return jQuery.ajax("/session/" + username, {
type: 'DELETE',
success: function(result) {
/* To keep lots of our variables unbound, we can handle a redirect on logging out.
*/
return window.location.reload();
}
});
},
/* fancy probes in ember
*/
insertProbes: function() {
var topLevel;
if (typeof console === "undefined" || console === null) {
return;
}
topLevel = function(fn, name) {
return window.probes.measure(fn, {
name: name,
before: function(data, owner, args) {
if (owner) {
return window.probes.clear();
}
},
after: function(data, owner, args) {
var ary, f, n, v, _ref;
if (owner && data.time > 10) {
f = function(name, data) {
if (data && data.count) {
return "" + name + " - " + data.count + " calls " + ((data.time + 0.0).toFixed(2)) + "ms";
} }
ary = []; };
_ref = window.probes; if (console && console.group) {
for (n in _ref) { console.group(f(name, data));
v = _ref[n]; } else {
if (n === name || v.time < 1) { console.log("");
continue; console.log(f(name, data));
} }
ary.push({ ary = [];
k: n, _ref = window.probes;
v: v for (n in _ref) {
v = _ref[n];
if (n === name || v.time < 1) {
continue;
}
ary.push({
k: n,
v: v
});
}
ary.sortBy(function(item) {
if (item.v && item.v.time) {
return -item.v.time;
} else {
return 0;
}
}).each(function(item) {
var output = f("" + item.k, item.v);
if (output) {
return console.log(output);
}
});
if (typeof console !== "undefined" && console !== null) {
if (typeof console.groupEnd === "function") {
console.groupEnd();
}
}
return window.probes.clear();
}
}
});
};
Ember.View.prototype.renderToBuffer = window.probes.measure(Ember.View.prototype.renderToBuffer, "renderToBuffer");
Discourse.routeTo = topLevel(Discourse.routeTo, "Discourse.routeTo");
Ember.run.end = topLevel(Ember.run.end, "Ember.run.end");
},
authenticationComplete: function(options) {
// TODO, how to dispatch this to the view without the container?
var loginView;
loginView = Discourse.__container__.lookup('controller:modal').get('currentView');
return loginView.authenticationComplete(options);
},
buildRoutes: function(builder) {
var oldBuilder;
oldBuilder = Discourse.routeBuilder;
Discourse.routeBuilder = function() {
if (oldBuilder) {
oldBuilder.call(this);
}
return builder.call(this);
};
},
start: function() {
this.bindDOMEvents();
Discourse.SiteSettings = PreloadStore.getStatic('siteSettings');
Discourse.MessageBus.start();
Discourse.KeyValueStore.init("discourse_", Discourse.MessageBus);
Discourse.insertProbes();
// subscribe to any site customizations that are loaded
$('link.custom-css').each(function() {
var id, split, stylesheet,
_this = this;
split = this.href.split("/");
id = split[split.length - 1].split(".css")[0];
stylesheet = this;
return Discourse.MessageBus.subscribe("/file-change/" + id, function(data) {
var orig, sp;
if (!$(stylesheet).data('orig')) {
$(stylesheet).data('orig', stylesheet.href);
}
orig = $(stylesheet).data('orig');
sp = orig.split(".css?");
stylesheet.href = sp[0] + ".css?" + data;
});
});
$('header.custom').each(function() {
var header;
header = $(this);
return Discourse.MessageBus.subscribe("/header-change/" + ($(this).data('key')), function(data) {
return header.html(data);
});
});
// possibly move this to dev only
return Discourse.MessageBus.subscribe("/file-change", function(data) {
Ember.TEMPLATES.empty = Handlebars.compile("<div></div>");
return data.each(function(me) {
var js;
if (me === "refresh") {
return document.location.reload(true);
} else if (me.name.substr(-10) === "handlebars") {
js = me.name.replace(".handlebars", "").replace("app/assets/javascripts", "/assets");
return $LAB.script(js + "?hash=" + me.hash).wait(function() {
var templateName;
templateName = js.replace(".js", "").replace("/assets/", "");
return jQuery.each(Ember.View.views, function() {
var _this = this;
if (this.get('templateName') === templateName) {
this.set('templateName', 'empty');
this.rerender();
return Em.run.next(function() {
_this.set('templateName', templateName);
return _this.rerender();
}); });
} }
ary.sortBy(function(item) { });
if (item.v && item.v.time) { });
return -item.v.time; } else {
} else { return $('link').each(function() {
return 0; if (this.href.match(me.name) && me.hash) {
} if (!$(this).data('orig')) {
}).each(function(item) { $(this).data('orig', this.href);
var output = f("" + item.k, item.v);
if (output) {
return console.log(output);
}
});
if (typeof console !== "undefined" && console !== null) {
if (typeof console.groupEnd === "function") {
console.groupEnd();
}
} }
return window.probes.clear(); this.href = $(this).data('orig') + "&hash=" + me.hash;
} }
} });
});
};
Ember.View.prototype.renderToBuffer = window.probes.measure(Ember.View.prototype.renderToBuffer, "renderToBuffer");
Discourse.routeTo = topLevel(Discourse.routeTo, "Discourse.routeTo");
Ember.run.end = topLevel(Ember.run.end, "Ember.run.end");
},
authenticationComplete: function(options) {
// TODO, how to dispatch this to the view without the container?
var loginView;
loginView = Discourse.__container__.lookup('controller:modal').get('currentView');
return loginView.authenticationComplete(options);
},
buildRoutes: function(builder) {
var oldBuilder;
oldBuilder = Discourse.routeBuilder;
Discourse.routeBuilder = function() {
if (oldBuilder) {
oldBuilder.call(this);
} }
return builder.call(this);
};
},
start: function() {
this.bindDOMEvents();
Discourse.SiteSettings = PreloadStore.getStatic('siteSettings');
Discourse.MessageBus.start();
Discourse.KeyValueStore.init("discourse_", Discourse.MessageBus);
Discourse.insertProbes();
// subscribe to any site customizations that are loaded
jQuery('link.custom-css').each(function() {
var id, split, stylesheet,
_this = this;
split = this.href.split("/");
id = split[split.length - 1].split(".css")[0];
stylesheet = this;
return Discourse.MessageBus.subscribe("/file-change/" + id, function(data) {
var orig, sp;
if (!jQuery(stylesheet).data('orig')) {
jQuery(stylesheet).data('orig', stylesheet.href);
}
orig = jQuery(stylesheet).data('orig');
sp = orig.split(".css?");
stylesheet.href = sp[0] + ".css?" + data;
});
});
jQuery('header.custom').each(function() {
var header;
header = jQuery(this);
return Discourse.MessageBus.subscribe("/header-change/" + (jQuery(this).data('key')), function(data) {
return header.html(data);
});
}); });
});
}
});
// possibly move this to dev only Discourse.Router = Discourse.Router.reopen({
return Discourse.MessageBus.subscribe("/file-change", function(data) { location: 'discourse_location'
Ember.TEMPLATES.empty = Handlebars.compile("<div></div>"); });
return data.each(function(me) {
var js;
if (me === "refresh") {
return document.location.reload(true);
} else if (me.name.substr(-10) === "handlebars") {
js = me.name.replace(".handlebars", "").replace("app/assets/javascripts", "/assets");
return $LAB.script(js + "?hash=" + me.hash).wait(function() {
var templateName;
templateName = js.replace(".js", "").replace("/assets/", "");
return jQuery.each(Ember.View.views, function() {
var _this = this;
if (this.get('templateName') === templateName) {
this.set('templateName', 'empty');
this.rerender();
return Em.run.next(function() {
_this.set('templateName', templateName);
return _this.rerender();
});
}
});
});
} else {
return jQuery('link').each(function() {
if (this.href.match(me.name) && me.hash) {
if (!jQuery(this).data('orig')) {
jQuery(this).data('orig', this.href);
}
this.href = jQuery(this).data('orig') + "&hash=" + me.hash;
}
});
}
});
});
}
});
window.Discourse.Router = Discourse.Router.reopen({ // since we have no jquery-rails these days, hook up csrf token
location: 'discourse_location' csrf_token = $('meta[name=csrf-token]').attr('content');
});
// since we have no jquery-rails these days, hook up csrf token jQuery.ajaxPrefilter(function(options, originalOptions, xhr) {
csrf_token = jQuery('meta[name=csrf-token]').attr('content'); if (!options.crossDomain) {
xhr.setRequestHeader('X-CSRF-Token', csrf_token);
}
});
jQuery.ajaxPrefilter(function(options, originalOptions, xhr) {
if (!options.crossDomain) {
xhr.setRequestHeader('X-CSRF-Token', csrf_token);
}
});
}).call(this);

View File

@ -1,313 +1,317 @@
(function() { /**
This is a jQuery plugin to support autocompleting values in our text fields.
(function($) { @module $.fn.autocomplete
var template; **/
template = null; $.fn.autocomplete = function(options) {
$.fn.autocomplete = function(options) {
var addInputSelectedItem, autocompleteOptions, closeAutocomplete, completeEnd, completeStart, completeTerm, div, height;
var inputSelectedItems, isInput, markSelected, me, oldClose, renderAutocomplete, selectedOption, updateAutoComplete, vals;
var width, wrap, _this = this;
if (this.length === 0) {
return;
}
if (options && options.cancel && this.data("closeAutocomplete")) {
this.data("closeAutocomplete")();
return this;
}
if (this.length !== 1) {
alert("only supporting one matcher at the moment");
}
autocompleteOptions = null;
selectedOption = null;
completeStart = null;
completeEnd = null;
me = this;
div = null;
/* input is handled differently
*/
isInput = this[0].tagName === "INPUT"; var addInputSelectedItem, autocompleteOptions, closeAutocomplete, completeEnd, completeStart, completeTerm, div, height;
inputSelectedItems = []; var inputSelectedItems, isInput, markSelected, me, oldClose, renderAutocomplete, selectedOption, updateAutoComplete, vals;
addInputSelectedItem = function(item) { var width, wrap, _this = this;
var d, prev, transformed;
if (options.transformComplete) { if (this.length === 0) return;
transformed = options.transformComplete(item);
if (options && options.cancel && this.data("closeAutocomplete")) {
this.data("closeAutocomplete")();
return this;
}
if (this.length !== 1) {
alert("only supporting one matcher at the moment");
}
autocompleteOptions = null;
selectedOption = null;
completeStart = null;
completeEnd = null;
me = this;
div = null;
// input is handled differently
isInput = this[0].tagName === "INPUT";
inputSelectedItems = [];
addInputSelectedItem = function(item) {
var d, prev, transformed;
if (options.transformComplete) {
transformed = options.transformComplete(item);
}
d = $("<div class='item'><span>" + (transformed || item) + "<a href='#'><i class='icon-remove'></i></a></span></div>");
prev = me.parent().find('.item:last');
if (prev.length === 0) {
me.parent().prepend(d);
} else {
prev.after(d);
}
inputSelectedItems.push(item);
if (options.onChangeItems) {
options.onChangeItems(inputSelectedItems);
}
return d.find('a').click(function() {
closeAutocomplete();
inputSelectedItems.splice(jQuery.inArray(item), 1);
$(this).parent().parent().remove();
if (options.onChangeItems) {
return options.onChangeItems(inputSelectedItems);
}
});
};
if (isInput) {
width = this.width();
height = this.height();
wrap = this.wrap("<div class='ac-wrap clearfix'/>").parent();
wrap.width(width);
this.width(80);
this.attr('name', this.attr('name') + "-renamed");
vals = this.val().split(",");
vals.each(function(x) {
if (x !== "") {
if (options.reverseTransform) {
x = options.reverseTransform(x);
} }
d = jQuery("<div class='item'><span>" + (transformed || item) + "<a href='#'><i class='icon-remove'></i></a></span></div>"); return addInputSelectedItem(x);
prev = me.parent().find('.item:last'); }
if (prev.length === 0) { });
me.parent().prepend(d); this.val("");
} else { completeStart = 0;
prev.after(d); wrap.click(function() {
} _this.focus();
inputSelectedItems.push(item); return true;
if (options.onChangeItems) { });
options.onChangeItems(inputSelectedItems); }
}
return d.find('a').click(function() { markSelected = function() {
closeAutocomplete(); var links;
inputSelectedItems.splice(jQuery.inArray(item), 1); links = div.find('li a');
jQuery(this).parent().parent().remove(); links.removeClass('selected');
if (options.onChangeItems) { return $(links[selectedOption]).addClass('selected');
return options.onChangeItems(inputSelectedItems); };
}
}); renderAutocomplete = function() {
var borderTop, mePos, pos, ul;
if (div) {
div.hide().remove();
}
if (autocompleteOptions.length === 0) {
return;
}
div = $(options.template({
options: autocompleteOptions
}));
ul = div.find('ul');
selectedOption = 0;
markSelected();
ul.find('li').click(function() {
selectedOption = ul.find('li').index(this);
completeTerm(autocompleteOptions[selectedOption]);
return false;
});
pos = null;
if (isInput) {
pos = {
left: 0,
top: 0
}; };
} else {
pos = me.caretPosition({
pos: completeStart,
key: options.key
});
}
div.css({
left: "-1000px"
});
me.parent().append(div);
mePos = me.position();
borderTop = parseInt(me.css('border-top-width'), 10) || 0;
return div.css({
position: 'absolute',
top: (mePos.top + pos.top - div.height() + borderTop) + 'px',
left: (mePos.left + pos.left + 27) + 'px'
});
};
updateAutoComplete = function(r) {
if (completeStart === null) return;
autocompleteOptions = r;
if (!r || r.length === 0) {
return closeAutocomplete();
} else {
return renderAutocomplete();
}
};
closeAutocomplete = function() {
if (div) {
div.hide().remove();
}
div = null;
completeStart = null;
autocompleteOptions = null;
};
// chain to allow multiples
oldClose = me.data("closeAutocomplete");
me.data("closeAutocomplete", function() {
if (oldClose) {
oldClose();
}
return closeAutocomplete();
});
completeTerm = function(term) {
var text;
if (term) {
if (isInput) { if (isInput) {
width = this.width(); me.val("");
height = this.height(); addInputSelectedItem(term);
wrap = this.wrap("<div class='ac-wrap clearfix'/>").parent(); } else {
wrap.width(width); if (options.transformComplete) {
this.width(80); term = options.transformComplete(term);
this.attr('name', this.attr('name') + "-renamed"); }
vals = this.val().split(","); text = me.val();
vals.each(function(x) { text = text.substring(0, completeStart) + (options.key || "") + term + ' ' + text.substring(completeEnd + 1, text.length);
if (x !== "") { me.val(text);
if (options.reverseTransform) { Discourse.Utilities.setCaretPosition(me[0], completeStart + 1 + term.length);
x = options.reverseTransform(x);
}
return addInputSelectedItem(x);
}
});
this.val("");
completeStart = 0;
wrap.click(function() {
_this.focus();
return true;
});
} }
markSelected = function() { }
var links; return closeAutocomplete();
links = div.find('li a'); };
links.removeClass('selected');
return jQuery(links[selectedOption]).addClass('selected'); $(this).keypress(function(e) {
}; var caretPosition, prevChar, term;
renderAutocomplete = function() { if (!options.key) {
var borderTop, mePos, pos, ul; return;
if (div) { }
div.hide().remove(); /* keep hunting backwards till you hit a
*/
if (e.which === options.key.charCodeAt(0)) {
caretPosition = Discourse.Utilities.caretPosition(me[0]);
prevChar = me.val().charAt(caretPosition - 1);
if (!prevChar || /\s/.test(prevChar)) {
completeStart = completeEnd = caretPosition;
term = "";
options.dataSource(term, updateAutoComplete);
}
}
});
return $(this).keydown(function(e) {
var c, caretPosition, i, initial, next, nextIsGood, prev, prevIsGood, stopFound, term, total, userToComplete;
if (!options.key) {
completeStart = 0;
}
if (e.which === 16) {
return;
}
if ((completeStart === null) && e.which === 8 && options.key) {
c = Discourse.Utilities.caretPosition(me[0]);
next = me[0].value[c];
nextIsGood = next === void 0 || /\s/.test(next);
c -= 1;
initial = c;
prevIsGood = true;
while (prevIsGood && c >= 0) {
c -= 1;
prev = me[0].value[c];
stopFound = prev === options.key;
if (stopFound) {
prev = me[0].value[c - 1];
if (!prev || /\s/.test(prev)) {
completeStart = c;
caretPosition = completeEnd = initial;
term = me[0].value.substring(c + 1, initial);
options.dataSource(term, updateAutoComplete);
return true;
}
} }
if (autocompleteOptions.length === 0) { prevIsGood = /[a-zA-Z\.]/.test(prev);
return; }
} }
div = jQuery(options.template({ if (e.which === 27) {
options: autocompleteOptions if (completeStart !== null) {
})); closeAutocomplete();
ul = div.find('ul'); return false;
selectedOption = 0; }
markSelected(); return true;
ul.find('li').click(function() { }
selectedOption = ul.find('li').index(this); if (completeStart !== null) {
completeTerm(autocompleteOptions[selectedOption]); caretPosition = Discourse.Utilities.caretPosition(me[0]);
return false; /* If we've backspaced past the beginning, cancel unless no key
});
pos = null;
if (isInput) {
pos = {
left: 0,
top: 0
};
} else {
pos = me.caretPosition({
pos: completeStart,
key: options.key
});
}
div.css({
left: "-1000px"
});
me.parent().append(div);
mePos = me.position();
borderTop = parseInt(me.css('border-top-width'), 10) || 0;
return div.css({
position: 'absolute',
top: (mePos.top + pos.top - div.height() + borderTop) + 'px',
left: (mePos.left + pos.left + 27) + 'px'
});
};
updateAutoComplete = function(r) {
if (completeStart === null) return;
autocompleteOptions = r;
if (!r || r.length === 0) {
return closeAutocomplete();
} else {
return renderAutocomplete();
}
};
closeAutocomplete = function() {
if (div) {
div.hide().remove();
}
div = null;
completeStart = null;
autocompleteOptions = null;
};
/* chain to allow multiples
*/ */
oldClose = me.data("closeAutocomplete"); if (caretPosition <= completeStart && options.key) {
me.data("closeAutocomplete", function() { closeAutocomplete();
if (oldClose) { return false;
oldClose(); }
} /* Keyboard codes! So 80's.
return closeAutocomplete(); */
});
completeTerm = function(term) { switch (e.which) {
var text; case 13:
if (term) { case 39:
if (isInput) { case 9:
me.val(""); if (!autocompleteOptions) {
addInputSelectedItem(term); return true;
}
if (selectedOption >= 0 && (userToComplete = autocompleteOptions[selectedOption])) {
completeTerm(userToComplete);
} else { } else {
if (options.transformComplete) { /* We're cancelling it, really.
term = options.transformComplete(term); */
}
text = me.val();
text = text.substring(0, completeStart) + (options.key || "") + term + ' ' + text.substring(completeEnd + 1, text.length);
me.val(text);
Discourse.Utilities.setCaretPosition(me[0], completeStart + 1 + term.length);
}
}
return closeAutocomplete();
};
jQuery(this).keypress(function(e) {
var caretPosition, prevChar, term;
if (!options.key) {
return;
}
/* keep hunting backwards till you hit a
*/
if (e.which === options.key.charCodeAt(0)) { return true;
caretPosition = Discourse.Utilities.caretPosition(me[0]);
prevChar = me.val().charAt(caretPosition - 1);
if (!prevChar || /\s/.test(prevChar)) {
completeStart = completeEnd = caretPosition;
term = "";
options.dataSource(term, updateAutoComplete);
} }
} closeAutocomplete();
}); return false;
return jQuery(this).keydown(function(e) { case 38:
var c, caretPosition, i, initial, next, nextIsGood, prev, prevIsGood, stopFound, term, total, userToComplete; selectedOption = selectedOption - 1;
if (!options.key) { if (selectedOption < 0) {
completeStart = 0; selectedOption = 0;
} }
if (e.which === 16) { markSelected();
return; return false;
} case 40:
if ((completeStart === null) && e.which === 8 && options.key) { total = autocompleteOptions.length;
c = Discourse.Utilities.caretPosition(me[0]); selectedOption = selectedOption + 1;
next = me[0].value[c]; if (selectedOption >= total) {
nextIsGood = next === void 0 || /\s/.test(next); selectedOption = total - 1;
c -= 1; }
initial = c; if (selectedOption < 0) {
prevIsGood = true; selectedOption = 0;
while (prevIsGood && c >= 0) { }
c -= 1; markSelected();
prev = me[0].value[c]; return false;
stopFound = prev === options.key; default:
if (stopFound) { /* otherwise they're typing - let's search for it!
prev = me[0].value[c - 1]; */
if (!prev || /\s/.test(prev)) {
completeStart = c; completeEnd = caretPosition;
caretPosition = completeEnd = initial; if (e.which === 8) {
term = me[0].value.substring(c + 1, initial); caretPosition--;
options.dataSource(term, updateAutoComplete); }
return true; if (caretPosition < 0) {
closeAutocomplete();
if (isInput) {
i = wrap.find('a:last');
if (i) {
i.click();
} }
} }
prevIsGood = /[a-zA-Z\.]/.test(prev);
}
}
if (e.which === 27) {
if (completeStart !== null) {
closeAutocomplete();
return false; return false;
} }
term = me.val().substring(completeStart + (options.key ? 1 : 0), caretPosition);
if (e.which > 48 && e.which < 90) {
term += String.fromCharCode(e.which);
} else {
if (e.which !== 8) {
term += ",";
}
}
options.dataSource(term, updateAutoComplete);
return true; return true;
} }
if (completeStart !== null) { }
caretPosition = Discourse.Utilities.caretPosition(me[0]); });
/* If we've backspaced past the beginning, cancel unless no key };
*/
if (caretPosition <= completeStart && options.key) {
closeAutocomplete();
return false;
}
/* Keyboard codes! So 80's.
*/
switch (e.which) {
case 13:
case 39:
case 9:
if (!autocompleteOptions) {
return true;
}
if (selectedOption >= 0 && (userToComplete = autocompleteOptions[selectedOption])) {
completeTerm(userToComplete);
} else {
/* We're cancelling it, really.
*/
return true;
}
closeAutocomplete();
return false;
case 38:
selectedOption = selectedOption - 1;
if (selectedOption < 0) {
selectedOption = 0;
}
markSelected();
return false;
case 40:
total = autocompleteOptions.length;
selectedOption = selectedOption + 1;
if (selectedOption >= total) {
selectedOption = total - 1;
}
if (selectedOption < 0) {
selectedOption = 0;
}
markSelected();
return false;
default:
/* otherwise they're typing - let's search for it!
*/
completeEnd = caretPosition;
if (e.which === 8) {
caretPosition--;
}
if (caretPosition < 0) {
closeAutocomplete();
if (isInput) {
i = wrap.find('a:last');
if (i) {
i.click();
}
}
return false;
}
term = me.val().substring(completeStart + (options.key ? 1 : 0), caretPosition);
if (e.which > 48 && e.which < 90) {
term += String.fromCharCode(e.which);
} else {
if (e.which !== 8) {
term += ",";
}
}
options.dataSource(term, updateAutoComplete);
return true;
}
}
});
};
return $.fn.autocomplete;
})(jQuery);
}).call(this);

View File

@ -1,222 +1,193 @@
/*global HANDLEBARS_TEMPLATES:true*/ /*global HANDLEBARS_TEMPLATES:true*/
(function() { /**
Support for BBCode rendering
Discourse.BBCode = { @class BBCode
QUOTE_REGEXP: /\[quote=([^\]]*)\]([\s\S]*?)\[\/quote\]/im, @namespace Discourse
/* Define our replacers @module Discourse
*/ **/
Discourse.BBCode = {
replacers: { QUOTE_REGEXP: /\[quote=([^\]]*)\]([\s\S]*?)\[\/quote\]/im,
base: {
withoutArgs: { // Define our replacers
"ol": function(_, content) { replacers: {
return "<ol>" + content + "</ol>"; base: {
}, withoutArgs: {
"li": function(_, content) { "ol": function(_, content) { return "<ol>" + content + "</ol>"; },
return "<li>" + content + "</li>"; "li": function(_, content) { return "<li>" + content + "</li>"; },
}, "ul": function(_, content) { return "<ul>" + content + "</ul>"; },
"ul": function(_, content) { "code": function(_, content) { return "<pre>" + content + "</pre>"; },
return "<ul>" + content + "</ul>"; "url": function(_, url) { return "<a href=\"" + url + "\">" + url + "</a>"; },
}, "email": function(_, address) { return "<a href=\"mailto:" + address + "\">" + address + "</a>"; },
"code": function(_, content) { "img": function(_, src) { return "<img src=\"" + src + "\">"; }
return "<pre>" + content + "</pre>"; },
}, withArgs: {
"url": function(_, url) { "url": function(_, href, title) { return "<a href=\"" + href + "\">" + title + "</a>"; },
return "<a href=\"" + url + "\">" + url + "</a>"; "email": function(_, address, title) { return "<a href=\"mailto:" + address + "\">" + title + "</a>"; },
}, "color": function(_, color, content) {
"email": function(_, address) { if (!/^(\#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?)|(aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|purple|red|silver|teal|white|yellow)$/.test(color)) {
return "<a href=\"mailto:" + address + "\">" + address + "</a>"; return content;
},
"img": function(_, src) {
return "<img src=\"" + src + "\">";
}
},
withArgs: {
"url": function(_, href, title) {
return "<a href=\"" + href + "\">" + title + "</a>";
},
"email": function(_, address, title) {
return "<a href=\"mailto:" + address + "\">" + title + "</a>";
},
"color": function(_, color, content) {
if (!/^(\#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?)|(aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|purple|red|silver|teal|white|yellow)$/.test(color)) {
return content;
}
return "<span style=\"color: " + color + "\">" + content + "</span>";
} }
return "<span style=\"color: " + color + "\">" + content + "</span>";
}
}
},
// For HTML emails
email: {
withoutArgs: {
"b": function(_, content) { return "<b>" + content + "</b>"; },
"i": function(_, content) { return "<i>" + content + "</i>"; },
"u": function(_, content) { return "<u>" + content + "</u>"; },
"s": function(_, content) { return "<s>" + content + "</s>"; },
"spoiler": function(_, content) { return "<span style='background-color: #000'>" + content + "</span>"; }
},
withArgs: {
"size": function(_, size, content) {
return "<span style=\"font-size: " + size + "px\">" + content + "</span>";
}
}
},
// For sane environments that support CSS
"default": {
withoutArgs: {
"b": function(_, content) { return "<span class='bbcode-b'>" + content + "</span>"; },
"i": function(_, content) { return "<span class='bbcode-i'>" + content + "</span>"; },
"u": function(_, content) { return "<span class='bbcode-u'>" + content + "</span>"; },
"s": function(_, content) { return "<span class='bbcode-s'>" + content + "</span>"; },
"spoiler": function(_, content) { return "<span class=\"spoiler\">" + content + "</span>";
} }
}, },
/* For HTML emails withArgs: {
*/ "size": function(_, size, content) {
return "<span class=\"bbcode-size-" + size + "\">" + content + "</span>";
email: {
withoutArgs: {
"b": function(_, content) {
return "<b>" + content + "</b>";
},
"i": function(_, content) {
return "<i>" + content + "</i>";
},
"u": function(_, content) {
return "<u>" + content + "</u>";
},
"s": function(_, content) {
return "<s>" + content + "</s>";
},
"spoiler": function(_, content) {
return "<span style='background-color: #000'>" + content + "</span>";
}
},
withArgs: {
"size": function(_, size, content) {
return "<span style=\"font-size: " + size + "px\">" + content + "</span>";
}
}
},
/* For sane environments that support CSS
*/
"default": {
withoutArgs: {
"b": function(_, content) {
return "<span class='bbcode-b'>" + content + "</span>";
},
"i": function(_, content) {
return "<span class='bbcode-i'>" + content + "</span>";
},
"u": function(_, content) {
return "<span class='bbcode-u'>" + content + "</span>";
},
"s": function(_, content) {
return "<span class='bbcode-s'>" + content + "</span>";
},
"spoiler": function(_, content) {
return "<span class=\"spoiler\">" + content + "</span>";
}
},
withArgs: {
"size": function(_, size, content) {
return "<span class=\"bbcode-size-" + size + "\">" + content + "</span>";
}
} }
} }
},
/* Apply a particular set of replacers */
apply: function(text, environment) {
var replacer;
replacer = Discourse.BBCode.parsedReplacers()[environment];
replacer.forEach(function(r) {
text = text.replace(r.regexp, r.fn);
});
return text;
},
parsedReplacers: function() {
var result;
if (this.parsed) {
return this.parsed;
}
result = {};
Object.keys(Discourse.BBCode.replacers, function(name, rules) {
var parsed;
parsed = result[name] = [];
Object.keys(Object.merge(Discourse.BBCode.replacers.base.withoutArgs, rules.withoutArgs), function(tag, val) {
return parsed.push({
regexp: new RegExp("\\[" + tag + "\\]([\\s\\S]*?)\\[\\/" + tag + "\\]", "igm"),
fn: val
});
});
return Object.keys(Object.merge(Discourse.BBCode.replacers.base.withArgs, rules.withArgs), function(tag, val) {
return parsed.push({
regexp: new RegExp("\\[" + tag + "=?(.+?)\\]([\\s\\S]*?)\\[\\/" + tag + "\\]", "igm"),
fn: val
});
});
});
this.parsed = result;
return this.parsed;
},
buildQuoteBBCode: function(post, contents) {
var contents_hashed, result, sansQuotes, stripped, stripped_hashed, tmp;
if (!contents) contents = "";
sansQuotes = contents.replace(this.QUOTE_REGEXP, '').trim();
if (sansQuotes.length === 0) return "";
/* Strip the HTML from cooked */
tmp = document.createElement('div');
tmp.innerHTML = post.get('cooked');
stripped = tmp.textContent || tmp.innerText;
/*
Let's remove any non alphanumeric characters as a kind of hash. Yes it's
not accurate but it should work almost every time we need it to. It would be unlikely
that the user would quote another post that matches in exactly this way.
*/
stripped_hashed = stripped.replace(/[^a-zA-Z0-9]/g, '');
contents_hashed = contents.replace(/[^a-zA-Z0-9]/g, '');
result = "[quote=\"" + (post.get('username')) + ", post:" + (post.get('post_number')) + ", topic:" + (post.get('topic_id'));
/* If the quote is the full message, attribute it as such */
if (stripped_hashed === contents_hashed) {
result += ", full:true";
}
result += "\"]\n" + sansQuotes + "\n[/quote]\n\n";
return result;
},
formatQuote: function(text, opts) {
/* Replace quotes with appropriate markup */
var args, matches, params, paramsSplit, paramsString, templateName, username;
while (matches = this.QUOTE_REGEXP.exec(text)) {
paramsString = matches[1];
paramsString = paramsString.replace(/\"/g, '');
paramsSplit = paramsString.split(/\, */);
params = [];
paramsSplit.each(function(p, i) {
var assignment;
if (i > 0) {
assignment = p.split(':');
if (assignment[0] && assignment[1]) {
return params.push({
key: assignment[0],
value: assignment[1].trim()
});
}
}
});
username = paramsSplit[0];
/* Arguments for formatting */
args = {
username: username,
params: params,
quote: matches[2].trim(),
avatarImg: opts.lookupAvatar ? opts.lookupAvatar(username) : void 0
};
templateName = 'quote';
if (opts && opts.environment) {
templateName = "quote_" + opts.environment;
}
text = text.replace(matches[0], "</p>" + HANDLEBARS_TEMPLATES[templateName](args) + "<p>");
}
return text;
},
format: function(text, opts) {
var environment;
if (opts && opts.environment) environment = opts.environment;
if (!environment) environment = 'default';
text = Discourse.BBCode.apply(text, environment);
// Add quotes
text = Discourse.BBCode.formatQuote(text, opts);
return text;
} }
}; },
}).call(this); // Apply a particular set of replacers
apply: function(text, environment) {
var replacer;
replacer = Discourse.BBCode.parsedReplacers()[environment];
replacer.forEach(function(r) {
text = text.replace(r.regexp, r.fn);
});
return text;
},
parsedReplacers: function() {
var result;
if (this.parsed) return this.parsed;
result = {};
Object.keys(Discourse.BBCode.replacers, function(name, rules) {
var parsed;
parsed = result[name] = [];
Object.keys(Object.merge(Discourse.BBCode.replacers.base.withoutArgs, rules.withoutArgs), function(tag, val) {
return parsed.push({
regexp: new RegExp("\\[" + tag + "\\]([\\s\\S]*?)\\[\\/" + tag + "\\]", "igm"),
fn: val
});
});
return Object.keys(Object.merge(Discourse.BBCode.replacers.base.withArgs, rules.withArgs), function(tag, val) {
return parsed.push({
regexp: new RegExp("\\[" + tag + "=?(.+?)\\]([\\s\\S]*?)\\[\\/" + tag + "\\]", "igm"),
fn: val
});
});
});
this.parsed = result;
return this.parsed;
},
buildQuoteBBCode: function(post, contents) {
var contents_hashed, result, sansQuotes, stripped, stripped_hashed, tmp;
if (!contents) contents = "";
sansQuotes = contents.replace(this.QUOTE_REGEXP, '').trim();
if (sansQuotes.length === 0) return "";
/* Strip the HTML from cooked */
tmp = document.createElement('div');
tmp.innerHTML = post.get('cooked');
stripped = tmp.textContent || tmp.innerText;
/*
Let's remove any non alphanumeric characters as a kind of hash. Yes it's
not accurate but it should work almost every time we need it to. It would be unlikely
that the user would quote another post that matches in exactly this way.
*/
stripped_hashed = stripped.replace(/[^a-zA-Z0-9]/g, '');
contents_hashed = contents.replace(/[^a-zA-Z0-9]/g, '');
result = "[quote=\"" + (post.get('username')) + ", post:" + (post.get('post_number')) + ", topic:" + (post.get('topic_id'));
/* If the quote is the full message, attribute it as such */
if (stripped_hashed === contents_hashed) {
result += ", full:true";
}
result += "\"]\n" + sansQuotes + "\n[/quote]\n\n";
return result;
},
formatQuote: function(text, opts) {
/* Replace quotes with appropriate markup */
var args, matches, params, paramsSplit, paramsString, templateName, username;
while (matches = this.QUOTE_REGEXP.exec(text)) {
paramsString = matches[1];
paramsString = paramsString.replace(/\"/g, '');
paramsSplit = paramsString.split(/\, */);
params = [];
paramsSplit.each(function(p, i) {
var assignment;
if (i > 0) {
assignment = p.split(':');
if (assignment[0] && assignment[1]) {
return params.push({
key: assignment[0],
value: assignment[1].trim()
});
}
}
});
username = paramsSplit[0];
/* Arguments for formatting */
args = {
username: username,
params: params,
quote: matches[2].trim(),
avatarImg: opts.lookupAvatar ? opts.lookupAvatar(username) : void 0
};
templateName = 'quote';
if (opts && opts.environment) {
templateName = "quote_" + opts.environment;
}
text = text.replace(matches[0], "</p>" + HANDLEBARS_TEMPLATES[templateName](args) + "<p>");
}
return text;
},
/**
Format a text string using BBCode
@method format
@param {String} text The text we want to format
@param {Object} opts Rendering options
**/
format: function(text, opts) {
var environment;
if (opts && opts.environment) environment = opts.environment;
if (!environment) environment = 'default';
text = Discourse.BBCode.apply(text, environment);
// Add quotes
text = Discourse.BBCode.formatQuote(text, opts);
return text;
}
};

View File

@ -1,135 +1,134 @@
// http://stackoverflow.com/questions/263743/how-to-get-caret-position-in-textarea
var clone, getCaret;
getCaret = function(el) {
var r, rc, re;
if (el.selectionStart) {
return el.selectionStart;
} else if (document.selection) {
el.focus();
r = document.selection.createRange();
if (!r) return 0;
re = el.createTextRange();
rc = re.duplicate();
re.moveToBookmark(r.getBookmark());
rc.setEndPoint("EndToStart", re);
return rc.text.length;
}
return 0;
};
/* caret position in textarea ... very hacky ... sorry clone = null;
*/
/**
This is a jQuery plugin to retrieve the caret position in a textarea
(function() { @module $.fn.caretPosition
**/
$.fn.caretPosition = function(options) {
var after, before, getStyles, guard, html, important, insertSpaceAfterBefore, letter, makeCursor, p, pPos, pos, span, styles, textarea, val;
if (clone) {
clone.remove();
}
span = $("#pos span");
textarea = $(this);
(function($) { getStyles = function(el, prop) {
/* http://stackoverflow.com/questions/263743/how-to-get-caret-position-in-textarea if (el.currentStyle) {
*/ return el.currentStyle;
} else {
return document.defaultView.getComputedStyle(el, "");
}
};
var clone, getCaret; styles = getStyles(textarea[0]);
getCaret = function(el) { clone = $("<div><p></p></div>").appendTo("body");
var r, rc, re; p = clone.find("p");
if (el.selectionStart) { clone.width(textarea.width());
return el.selectionStart; clone.height(textarea.height());
} else if (document.selection) {
el.focus();
r = document.selection.createRange();
if (!r) return 0;
re = el.createTextRange();
rc = re.duplicate();
re.moveToBookmark(r.getBookmark());
rc.setEndPoint("EndToStart", re);
return rc.text.length;
}
return 0;
};
clone = null;
$.fn.caretPosition = function(options) {
var after, before, getStyles, guard, html, important, insertSpaceAfterBefore, letter, makeCursor, p, pPos, pos, span, styles, textarea, val;
if (clone) {
clone.remove();
}
span = jQuery("#pos span");
textarea = jQuery(this);
getStyles = function(el, prop) {
if (el.currentStyle) {
return el.currentStyle;
} else {
return document.defaultView.getComputedStyle(el, "");
}
};
styles = getStyles(textarea[0]);
clone = jQuery("<div><p></p></div>").appendTo("body");
p = clone.find("p");
clone.width(textarea.width());
clone.height(textarea.height());
important = function(prop) {
return styles.getPropertyValue(prop);
};
clone.css({
border: "1px solid black",
padding: important("padding"),
resize: important("resize"),
"max-height": textarea.height() + "px",
"overflow-y": "auto",
"word-wrap": "break-word",
position: "absolute",
left: "-7000px"
});
p.css({
margin: 0,
padding: 0,
"word-wrap": "break-word",
"letter-spacing": important("letter-spacing"),
"font-family": important("font-family"),
"font-size": important("font-size"),
"line-height": important("line-height")
});
before = void 0;
after = void 0;
pos = options && (options.pos || options.pos === 0) ? options.pos : getCaret(textarea[0]);
val = textarea.val().replace("\r", "");
if (options && options.key) {
val = val.substring(0, pos) + options.key + val.substring(pos);
}
before = pos - 1;
after = pos;
insertSpaceAfterBefore = false;
/* if before and after are \n insert a space
*/
if (val[before] === "\n" && val[after] === "\n") { important = function(prop) {
insertSpaceAfterBefore = true; return styles.getPropertyValue(prop);
} };
guard = function(v) {
var buf;
buf = v.replace(/</g, "&lt;");
buf = buf.replace(/>/g, "&gt;");
buf = buf.replace(/[ ]/g, "&#x200b;&nbsp;&#x200b;");
return buf.replace(/\n/g, "<br />");
};
makeCursor = function(pos, klass, color) {
var l;
l = val.substring(pos, pos + 1);
if (l === "\n") {
return "<br>";
}
return "<span class='" + klass + "' style='background-color:" + color + "; margin:0; padding: 0'>" + guard(l) + "</span>";
};
html = "";
if (before >= 0) {
html += guard(val.substring(0, pos - 1)) + makeCursor(before, "before", "#d0ffff");
if (insertSpaceAfterBefore) {
html += makeCursor(0, "post-before", "#d0ffff");
}
}
if (after >= 0) {
html += makeCursor(after, "after", "#ffd0ff");
if (after - 1 < val.length) {
html += guard(val.substring(after + 1));
}
}
p.html(html);
clone.scrollTop(textarea.scrollTop());
letter = p.find("span:first");
pos = letter.offset();
if (letter.hasClass("before")) {
pos.left = pos.left + letter.width();
}
pPos = p.offset();
return {
/*clone.hide().remove()
*/
left: pos.left - pPos.left, clone.css({
top: (pos.top - pPos.top) - clone.scrollTop() border: "1px solid black",
}; padding: important("padding"),
}; resize: important("resize"),
return $.fn.caretPosition; "max-height": textarea.height() + "px",
"overflow-y": "auto",
})(jQuery); "word-wrap": "break-word",
position: "absolute",
left: "-7000px"
});
}).call(this); p.css({
margin: 0,
padding: 0,
"word-wrap": "break-word",
"letter-spacing": important("letter-spacing"),
"font-family": important("font-family"),
"font-size": important("font-size"),
"line-height": important("line-height")
});
before = void 0;
after = void 0;
pos = options && (options.pos || options.pos === 0) ? options.pos : getCaret(textarea[0]);
val = textarea.val().replace("\r", "");
if (options && options.key) {
val = val.substring(0, pos) + options.key + val.substring(pos);
}
before = pos - 1;
after = pos;
insertSpaceAfterBefore = false;
// if before and after are \n insert a space
if (val[before] === "\n" && val[after] === "\n") {
insertSpaceAfterBefore = true;
}
guard = function(v) {
var buf;
buf = v.replace(/</g, "&lt;");
buf = buf.replace(/>/g, "&gt;");
buf = buf.replace(/[ ]/g, "&#x200b;&nbsp;&#x200b;");
return buf.replace(/\n/g, "<br />");
};
makeCursor = function(pos, klass, color) {
var l;
l = val.substring(pos, pos + 1);
if (l === "\n") return "<br>";
return "<span class='" + klass + "' style='background-color:" + color + "; margin:0; padding: 0'>" + guard(l) + "</span>";
};
html = "";
if (before >= 0) {
html += guard(val.substring(0, pos - 1)) + makeCursor(before, "before", "#d0ffff");
if (insertSpaceAfterBefore) {
html += makeCursor(0, "post-before", "#d0ffff");
}
}
if (after >= 0) {
html += makeCursor(after, "after", "#ffd0ff");
if (after - 1 < val.length) {
html += guard(val.substring(after + 1));
}
}
p.html(html);
clone.scrollTop(textarea.scrollTop());
letter = p.find("span:first");
pos = letter.offset();
if (letter.hasClass("before")) {
pos.left = pos.left + letter.width();
}
pPos = p.offset();
return {
left: pos.left - pPos.left,
top: (pos.top - pPos.top) - clone.scrollTop()
};
};

View File

@ -1,108 +1,101 @@
/**
Used for tracking when the user clicks on a link
/* We use this object to keep track of click counts. @class ClickTrack
*/ @namespace Discourse
@module Discourse
**/
Discourse.ClickTrack = {
/**
Track a click on a link
(function() { @method trackClick
@param {jQuery.Event} e The click event that occurred
**/
trackClick: function(e) {
var $a, $article, $badge, count, destination, href, ownLink, postId, topicId, trackingUrl, userId;
$a = $(e.currentTarget);
if ($a.hasClass('lightbox')) {
return;
}
e.preventDefault();
window.Discourse.ClickTrack = { // We don't track clicks on quote back buttons
/* Pass the event of the click here and we'll do the magic! if ($a.hasClass('back') || $a.hasClass('quote-other-topic')) return true;
*/
trackClick: function(e) { // Remove the href, put it as a data attribute
var $a, $article, $badge, count, destination, href, ownLink, postId, topicId, trackingUrl, userId; if (!$a.data('href')) {
$a = jQuery(e.currentTarget); $a.addClass('no-href');
if ($a.hasClass('lightbox')) { $a.data('href', $a.attr('href'));
return; $a.attr('href', null);
// Don't route to this URL
$a.data('auto-route', true);
}
href = $a.data('href');
$article = $a.closest('article');
postId = $article.data('post-id');
topicId = $('#topic').data('topic-id');
userId = $a.data('user-id');
if (!userId) {
userId = $article.data('user-id');
}
ownLink = userId && (userId === Discourse.get('currentUser.id'));
// Build a Redirect URL
trackingUrl = "/clicks/track?url=" + encodeURIComponent(href);
if (postId && (!$a.data('ignore-post-id'))) {
trackingUrl += "&post_id=" + encodeURI(postId);
}
if (topicId) {
trackingUrl += "&topic_id=" + encodeURI(topicId);
}
// Update badge clicks unless it's our own
if (!ownLink) {
$badge = $('span.badge', $a);
if ($badge.length === 1) {
count = parseInt($badge.html(), 10);
$badge.html(count + 1);
} }
e.preventDefault(); }
/* We don't track clicks on quote back buttons
*/
if ($a.hasClass('back') || $a.hasClass('quote-other-topic')) { // If they right clicked, change the destination href
return true; if (e.which === 3) {
} destination = Discourse.SiteSettings.track_external_right_clicks ? trackingUrl : href;
/* Remove the href, put it as a data attribute $a.attr('href', destination);
*/ return true;
}
if (!$a.data('href')) { // if they want to open in a new tab, do an AJAX request
$a.addClass('no-href'); if (e.metaKey || e.ctrlKey || e.which === 2) {
$a.data('href', $a.attr('href')); jQuery.get("/clicks/track", {
$a.attr('href', null); url: href,
/* Don't route to this URL post_id: postId,
*/ topic_id: topicId,
redirect: false
$a.data('auto-route', true); });
} window.open(href, '_blank');
href = $a.data('href');
$article = $a.closest('article');
postId = $article.data('post-id');
topicId = jQuery('#topic').data('topic-id');
userId = $a.data('user-id');
if (!userId) {
userId = $article.data('user-id');
}
ownLink = userId && (userId === Discourse.get('currentUser.id'));
/* Build a Redirect URL
*/
trackingUrl = "/clicks/track?url=" + encodeURIComponent(href);
if (postId && (!$a.data('ignore-post-id'))) {
trackingUrl += "&post_id=" + encodeURI(postId);
}
if (topicId) {
trackingUrl += "&topic_id=" + encodeURI(topicId);
}
/* Update badge clicks unless it's our own
*/
if (!ownLink) {
$badge = jQuery('span.badge', $a);
if ($badge.length === 1) {
count = parseInt($badge.html(), 10);
$badge.html(count + 1);
}
}
/* If they right clicked, change the destination href
*/
if (e.which === 3) {
destination = Discourse.SiteSettings.track_external_right_clicks ? trackingUrl : href;
$a.attr('href', destination);
return true;
}
/* if they want to open in a new tab, do an AJAX request
*/
if (e.metaKey || e.ctrlKey || e.which === 2) {
jQuery.get("/clicks/track", {
url: href,
post_id: postId,
topic_id: topicId,
redirect: false
});
window.open(href, '_blank');
return false;
}
/* If we're on the same site, use the router and track via AJAX
*/
if (href.indexOf(window.location.origin) === 0) {
jQuery.get("/clicks/track", {
url: href,
post_id: postId,
topic_id: topicId,
redirect: false
});
Discourse.routeTo(href);
return false;
}
/* Otherwise, use a custom URL with a redirect
*/
window.location = trackingUrl;
return false; return false;
} }
};
}).call(this); // If we're on the same site, use the router and track via AJAX
if (href.indexOf(window.location.origin) === 0) {
jQuery.get("/clicks/track", {
url: href,
post_id: postId,
topic_id: topicId,
redirect: false
});
Discourse.routeTo(href);
return false;
}
// Otherwise, use a custom URL with a redirect
window.location = trackingUrl;
return false;
}
};

View File

@ -1,4 +1,14 @@
window.Discourse.debounce = function(func, wait, trickle) { /**
Debounce a Javascript function. This means if it's called many times in a time limit it
should only be executed once.
@method debounce
@module Discourse
@param {function} func The function to debounce
@param {Numbers} wait how long to wait
@param {Boolean} trickle
**/
Discourse.debounce = function(func, wait, trickle) {
var timeout; var timeout;
timeout = null; timeout = null;
@ -12,7 +22,7 @@ window.Discourse.debounce = function(func, wait, trickle) {
}; };
if (timeout && trickle) { if (timeout && trickle) {
/* already queued, let it through */ // already queued, let it through
return; return;
} }

View File

@ -1,10 +0,0 @@
(function() {
Discourse.TextField = Ember.TextField.extend({
attributeBindings: ['autocorrect', 'autocapitalize'],
placeholder: (function() {
return Em.String.i18n(this.get('placeholderKey'));
}).property('placeholderKey')
});
}).call(this);

View File

@ -1,92 +1,91 @@
/**
This is a jQuery plugin to support resizing text areas.
/*based off text area resizer by Ryan O'Dell : http://plugins.jquery.com/misc/textarea.js Originally based off text area resizer by Ryan O'Dell : http://plugins.jquery.com/misc/textarea.js
*/
@module $.fn.DivResizer
**/
(function() { var div, endDrag, grip, lastMousePos, min, mousePosition, originalDivHeight, originalPos, performDrag, startDrag, wrappedEndDrag, wrappedPerformDrag;
div = void 0;
originalPos = void 0;
originalDivHeight = void 0;
lastMousePos = 0;
min = 230;
grip = void 0;
wrappedEndDrag = void 0;
wrappedPerformDrag = void 0;
(function($) { startDrag = function(e, opts) {
var div, endDrag, grip, lastMousePos, min, mousePosition, originalDivHeight, originalPos, performDrag, startDrag, wrappedEndDrag, wrappedPerformDrag; div = $(e.data.el);
div = void 0; div.addClass('clear-transitions');
originalPos = void 0; div.blur();
originalDivHeight = void 0; lastMousePos = mousePosition(e).y;
lastMousePos = 0; originalPos = lastMousePos;
min = 230; originalDivHeight = div.height();
grip = void 0; wrappedPerformDrag = (function() {
wrappedEndDrag = void 0; return function(e) {
wrappedPerformDrag = void 0; return performDrag(e, opts);
startDrag = function(e, opts) {
div = jQuery(e.data.el);
div.addClass('clear-transitions');
div.blur();
lastMousePos = mousePosition(e).y;
originalPos = lastMousePos;
originalDivHeight = div.height();
wrappedPerformDrag = (function() {
return function(e) {
return performDrag(e, opts);
};
})();
wrappedEndDrag = (function() {
return function(e) {
return endDrag(e, opts);
};
})();
jQuery(document).mousemove(wrappedPerformDrag).mouseup(wrappedEndDrag);
return false;
}; };
performDrag = function(e, opts) { })();
var size, sizePx, thisMousePos; wrappedEndDrag = (function() {
thisMousePos = mousePosition(e).y; return function(e) {
size = originalDivHeight + (originalPos - thisMousePos); return endDrag(e, opts);
lastMousePos = thisMousePos;
size = Math.min(size, jQuery(window).height());
size = Math.max(min, size);
sizePx = size + "px";
if (typeof opts.onDrag === "function") {
opts.onDrag(sizePx);
}
div.height(sizePx);
if (size < min) {
endDrag(e, opts);
}
return false;
}; };
endDrag = function(e, opts) { })();
jQuery(document).unbind("mousemove", wrappedPerformDrag).unbind("mouseup", wrappedEndDrag); $(document).mousemove(wrappedPerformDrag).mouseup(wrappedEndDrag);
div.removeClass('clear-transitions'); return false;
div.focus(); };
if (typeof opts.resize === "function") {
opts.resize(); performDrag = function(e, opts) {
} var size, sizePx, thisMousePos;
div = null; thisMousePos = mousePosition(e).y;
}; size = originalDivHeight + (originalPos - thisMousePos);
mousePosition = function(e) { lastMousePos = thisMousePos;
return { size = Math.min(size, $(window).height());
x: e.clientX + document.documentElement.scrollLeft, size = Math.max(min, size);
y: e.clientY + document.documentElement.scrollTop sizePx = size + "px";
if (typeof opts.onDrag === "function") {
opts.onDrag(sizePx);
}
div.height(sizePx);
if (size < min) {
endDrag(e, opts);
}
return false;
};
endDrag = function(e, opts) {
$(document).unbind("mousemove", wrappedPerformDrag).unbind("mouseup", wrappedEndDrag);
div.removeClass('clear-transitions');
div.focus();
if (typeof opts.resize === "function") {
opts.resize();
}
div = null;
};
mousePosition = function(e) {
return {
x: e.clientX + document.documentElement.scrollLeft,
y: e.clientY + document.documentElement.scrollTop
};
};
$.fn.DivResizer = function(opts) {
return this.each(function() {
var grippie, start, staticOffset;
div = $(this);
if (div.hasClass("processed")) return;
div.addClass("processed");
staticOffset = null;
start = function() {
return function(e) {
return startDrag(e, opts);
}; };
}; };
$.fn.DivResizer = function(opts) { grippie = div.prepend("<div class='grippie'></div>").find('.grippie').bind("mousedown", {
return this.each(function() { el: this
var grippie, start, staticOffset; }, start());
div = jQuery(this); });
if (div.hasClass("processed")) { };
return;
}
div.addClass("processed");
staticOffset = null;
start = function() {
return function(e) {
return startDrag(e, opts);
};
};
grippie = div.prepend("<div class='grippie'></div>").find('.grippie').bind("mousedown", {
el: this
}, start());
});
};
return $.fn.DivResizer;
})(jQuery);
}).call(this);

View File

@ -1,129 +1,97 @@
/**
Track visible elemnts on the screen.
/* Track visible elements on the screen You can register for triggers on:
*/
`focusChanged` the top element we're focusing on
/* You can register for triggers on: `seenElement` if we've seen the element
*/
@class Eyeline
@namespace Discourse
@module Discourse
@uses RSVP.EventTarget
**/
Discourse.Eyeline = function Eyeline(selector) {
this.selector = selector;
}
/* focusChanged: -> the top element we're focusing on /**
*/ Call this whenever you want to consider what is being seen by the browser
@method update
**/
Discourse.Eyeline.prototype.update = function() {
var $elements, $results, atBottom, bottomOffset, docViewBottom, docViewTop, documentHeight, foundElement, windowHeight,
_this = this;
/* seenElement: -> if we've seen the element docViewTop = $(window).scrollTop();
*/ windowHeight = $(window).height();
docViewBottom = docViewTop + windowHeight;
documentHeight = $(document).height();
$elements = $(this.selector);
atBottom = false;
if (bottomOffset = $elements.last().offset()) {
atBottom = (bottomOffset.top <= docViewBottom) && (bottomOffset.top >= docViewTop);
}
(function() { // Whether we've seen any elements in this search
foundElement = false;
$results = $(this.selector);
return $results.each(function(i, elem) {
var $elem, elemBottom, elemTop, markSeen;
$elem = $(elem);
elemTop = $elem.offset().top;
elemBottom = elemTop + $elem.height();
markSeen = false;
// It's seen if...
Discourse.Eyeline = (function() { // ...the element is vertically within the top and botom
if ((elemTop <= docViewBottom) && (elemTop >= docViewTop)) markSeen = true;
function Eyeline(selector) { // ...the element top is above the top and the bottom is below the bottom (large elements)
this.selector = selector; if ((elemTop <= docViewTop) && (elemBottom >= docViewBottom)) markSeen = true;
}
/* Call this whenever we want to consider what is currently being seen by the browser // ...we're at the bottom and the bottom of the element is visible (large bottom elements)
*/ if (atBottom && (elemBottom >= docViewTop)) markSeen = true;
if (!markSeen) return true;
Eyeline.prototype.update = function() { // If you hit the bottom we mark all the elements as seen. Otherwise, just the first one
var $elements, $results, atBottom, bottomOffset, docViewBottom, docViewTop, documentHeight, foundElement, windowHeight, if (!atBottom) {
_this = this; _this.trigger('saw', {
docViewTop = jQuery(window).scrollTop(); detail: $elem
windowHeight = jQuery(window).height(); });
docViewBottom = docViewTop + windowHeight; if (i === 0) {
documentHeight = jQuery(document).height(); _this.trigger('sawTop', { detail: $elem });
$elements = jQuery(this.selector);
atBottom = false;
if (bottomOffset = $elements.last().offset()) {
atBottom = (bottomOffset.top <= docViewBottom) && (bottomOffset.top >= docViewTop);
} }
/* Whether we've seen any elements in this search return false;
*/ }
if (i === 0) {
foundElement = false; _this.trigger('sawTop', { detail: $elem });
$results = jQuery(this.selector); }
return $results.each(function(i, elem) { if (i === ($results.length - 1)) {
var $elem, elemBottom, elemTop, markSeen; return _this.trigger('sawBottom', { detail: $elem });
$elem = jQuery(elem); }
elemTop = $elem.offset().top; });
elemBottom = elemTop + $elem.height(); };
markSeen = false;
/* It's seen if...
*/
/* ...the element is vertically within the top and botom
*/
if ((elemTop <= docViewBottom) && (elemTop >= docViewTop)) {
markSeen = true;
}
/* ...the element top is above the top and the bottom is below the bottom (large elements)
*/
if ((elemTop <= docViewTop) && (elemBottom >= docViewBottom)) {
markSeen = true;
}
/* ...we're at the bottom and the bottom of the element is visible (large bottom elements)
*/
if (atBottom && (elemBottom >= docViewTop)) {
markSeen = true;
}
if (!markSeen) {
return true;
}
/* If you hit the bottom we mark all the elements as seen. Otherwise, just the first one
*/
if (!atBottom) {
_this.trigger('saw', {
detail: $elem
});
if (i === 0) {
_this.trigger('sawTop', {
detail: $elem
});
}
return false;
}
if (i === 0) {
_this.trigger('sawTop', {
detail: $elem
});
}
if (i === ($results.length - 1)) {
return _this.trigger('sawBottom', {
detail: $elem
});
}
});
};
/* Call this when we know aren't loading any more elements. Mark the rest
*/
/* as seen /**
*/ Call this when we know aren't loading any more elements. Mark the rest as seen
@method flushRest
**/
Discourse.Eyeline.prototype.flushRest = function() {
var _this = this;
return $(this.selector).each(function(i, elem) {
var $elem;
$elem = $(elem);
return _this.trigger('saw', { detail: $elem });
});
};
RSVP.EventTarget.mixin(Discourse.Eyeline.prototype);
Eyeline.prototype.flushRest = function() {
var _this = this;
return jQuery(this.selector).each(function(i, elem) {
var $elem;
$elem = jQuery(elem);
return _this.trigger('saw', {
detail: $elem
});
});
};
return Eyeline;
})();
RSVP.EventTarget.mixin(Discourse.Eyeline.prototype);
}).call(this);

View File

@ -1,50 +1,51 @@
/**
A simple key value store that uses LocalStorage
/* key value store @class KeyValueStore
*/ @namespace Discourse
@module Discourse
**/
Discourse.KeyValueStore = {
initialized: false,
context: "",
init: function(ctx, messageBus) {
initialized = true;
context = ctx;
},
(function() { abandonLocal: function() {
var i, k;
window.Discourse.KeyValueStore = (function() { if (!(localStorage && initialized)) {
var context, initialized; return;
initialized = false; }
context = ""; i = localStorage.length - 1;
return { while (i >= 0) {
init: function(ctx, messageBus) { k = localStorage.key(i);
initialized = true; if (k.substring(0, context.length) === context) {
context = ctx; localStorage.removeItem(k);
},
abandonLocal: function() {
var i, k;
if (!(localStorage && initialized)) {
return;
}
i = localStorage.length - 1;
while (i >= 0) {
k = localStorage.key(i);
if (k.substring(0, context.length) === context) {
localStorage.removeItem(k);
}
i--;
}
return true;
},
remove: function(key) {
return localStorage.removeItem(context + key);
},
set: function(opts) {
if (!(localStorage && initialized)) {
return false;
}
localStorage[context + opts.key] = opts.value;
},
get: function(key) {
if (!localStorage) {
return null;
}
return localStorage[context + key];
} }
}; i--;
})(); }
return true;
},
remove: function(key) {
return localStorage.removeItem(context + key);
},
set: function(opts) {
if (!(localStorage && initialized)) {
return false;
}
localStorage[context + opts.key] = opts.value;
},
get: function(key) {
if (!localStorage) {
return null;
}
return localStorage[context + key];
}
}
}).call(this);

View File

@ -1,23 +1,19 @@
/**
Helper object for lightboxes.
/* Helper object for light boxes. Uses highlight.js which is loaded @class Lightbox
*/ @namespace Discourse
@module Discourse
**/
/* on demand. Discourse.Lightbox = {
*/ apply: function($elem) {
var _this = this;
return $('a.lightbox', $elem).each(function(i, e) {
(function() { return $LAB.script("/javascripts/jquery.colorbox-min.js").wait(function() {
return $(e).colorbox();
window.Discourse.Lightbox = {
apply: function($elem) {
var _this = this;
return jQuery('a.lightbox', $elem).each(function(i, e) {
return $LAB.script("/javascripts/jquery.colorbox-min.js").wait(function() {
return jQuery(e).colorbox();
});
}); });
} });
}; }
}
}).call(this);

View File

@ -0,0 +1,59 @@
/**
Helps us determine whether someone has been mentioned by looking up their username.
@class Mention
@namespace Discourse
@module Discourse
**/
Discourse.Mention = (function() {
var cache, load, localCache, lookup, lookupCache;
localCache = {};
cache = function(name, valid) {
localCache[name] = valid;
};
lookupCache = function(name) {
return localCache[name];
};
lookup = function(name, callback) {
var cached;
cached = lookupCache(name);
if (cached === true || cached === false) {
callback(cached);
return false;
} else {
jQuery.get("/users/is_local_username", {
username: name
}, function(r) {
cache(name, r.valid);
return callback(r.valid);
});
return true;
}
};
load = function(e) {
var $elem, loading, username;
$elem = $(e);
if ($elem.data('mention-tested')) {
return;
}
username = $elem.text();
username = username.substr(1);
loading = lookup(username, function(valid) {
if (valid) {
return $elem.replaceWith("<a href='/users/" + (username.toLowerCase()) + "' class='mention'>@" + username + "</a>");
} else {
return $elem.removeClass('mention-loading').addClass('mention-tested');
}
});
if (loading) {
return $elem.addClass('mention-loading');
}
};
return {
load: load,
lookup: lookup,
lookupCache: lookupCache
};
})();

View File

@ -1,159 +1,157 @@
/*jshint bitwise: false*/ /*jshint bitwise: false*/
(function() {
window.Discourse.MessageBus = (function() { /**
/* http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript Message Bus functionality.
*/
var callbacks, clientId, failCount, interval, isHidden, queue, responseCallbacks, uniqueId; @class MessageBus
uniqueId = function() { @namespace Discourse
return 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, function(c) { @module Discourse
var r, v; **/
r = Math.random() * 16 | 0; Discourse.MessageBus = (function() {
v = c === 'x' ? r : (r & 0x3 | 0x8); // http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript
return v.toString(16); var callbacks, clientId, failCount, interval, isHidden, queue, responseCallbacks, uniqueId;
});
};
clientId = uniqueId();
responseCallbacks = {};
callbacks = [];
queue = [];
interval = null;
failCount = 0;
isHidden = function() {
if (document.hidden !== void 0) {
return document.hidden;
} else if (document.webkitHidden !== void 0) {
return document.webkitHidden;
} else if (document.msHidden !== void 0) {
return document.msHidden;
} else if (document.mozHidden !== void 0) {
return document.mozHidden;
} else {
/* fallback to problamatic window.focus
*/
return !Discourse.get('hasFocus'); uniqueId = function() {
} return 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
}; var r, v;
return { r = Math.random() * 16 | 0;
enableLongPolling: true, v = c === 'x' ? r : (r & 0x3 | 0x8);
callbackInterval: 60000, return v.toString(16);
maxPollInterval: 3 * 60 * 1000, });
callbacks: callbacks, };
clientId: clientId,
/*TODO
*/
stop: false, clientId = uniqueId();
/* Start polling responseCallbacks = {};
*/ callbacks = [];
queue = [];
interval = null;
failCount = 0;
start: function(opts) { isHidden = function() {
var poll, if (document.hidden !== void 0) {
_this = this; return document.hidden;
if (!opts) opts = {}; } else if (document.webkitHidden !== void 0) {
poll = function() { return document.webkitHidden;
var data, gotData; } else if (document.msHidden !== void 0) {
if (callbacks.length === 0) { return document.msHidden;
setTimeout(poll, 500); } else if (document.mozHidden !== void 0) {
return; return document.mozHidden;
} } else {
data = {}; // fallback to problamatic window.focus
callbacks.each(function(c) { return !Discourse.get('hasFocus');
data[c.channel] = c.last_id === void 0 ? -1 : c.last_id; }
}); };
gotData = false;
_this.longPoll = jQuery.ajax("/message-bus/" + clientId + "/poll?" + (isHidden() || !_this.enableLongPolling ? "dlp=t" : ""), { return {
data: data, enableLongPolling: true,
cache: false, callbackInterval: 60000,
dataType: 'json', maxPollInterval: 3 * 60 * 1000,
type: 'POST', callbacks: callbacks,
headers: { clientId: clientId,
'X-SILENCE-LOGGER': 'true'
}, stop: false,
success: function(messages) {
failCount = 0; // Start polling
return messages.each(function(message) { start: function(opts) {
gotData = true; var poll,
return callbacks.each(function(callback) { _this = this;
if (callback.channel === message.channel) { if (!opts) opts = {};
callback.last_id = message.message_id;
callback.func(message.data); poll = function() {
var data, gotData;
if (callbacks.length === 0) {
setTimeout(poll, 500);
return;
}
data = {};
callbacks.each(function(c) {
data[c.channel] = c.last_id === void 0 ? -1 : c.last_id;
});
gotData = false;
_this.longPoll = jQuery.ajax("/message-bus/" + clientId + "/poll?" + (isHidden() || !_this.enableLongPolling ? "dlp=t" : ""), {
data: data,
cache: false,
dataType: 'json',
type: 'POST',
headers: {
'X-SILENCE-LOGGER': 'true'
},
success: function(messages) {
failCount = 0;
return messages.each(function(message) {
gotData = true;
return callbacks.each(function(callback) {
if (callback.channel === message.channel) {
callback.last_id = message.message_id;
callback.func(message.data);
}
if (message.channel === "/__status") {
if (message.data[callback.channel] !== void 0) {
callback.last_id = message.data[callback.channel];
} }
if (message.channel === "/__status") { }
if (message.data[callback.channel] !== void 0) {
callback.last_id = message.data[callback.channel];
}
}
});
}); });
}, });
error: failCount += 1, },
complete: function() { error: failCount += 1,
if (gotData) { complete: function() {
setTimeout(poll, 100); if (gotData) {
} else { setTimeout(poll, 100);
interval = _this.callbackInterval; } else {
if (failCount > 2) { interval = _this.callbackInterval;
interval = interval * failCount; if (failCount > 2) {
} else if (isHidden()) { interval = interval * failCount;
/* slowning down stuff a lot when hidden } else if (isHidden()) {
*/ /* slowning down stuff a lot when hidden
*/
/* we will need to add a lot of fine tuning here /* we will need to add a lot of fine tuning here
*/ */
interval = interval * 4; interval = interval * 4;
}
if (interval > _this.maxPollInterval) {
interval = _this.maxPollInterval;
}
setTimeout(poll, interval);
} }
_this.longPoll = null; if (interval > _this.maxPollInterval) {
interval = _this.maxPollInterval;
}
setTimeout(poll, interval);
} }
}); _this.longPoll = null;
};
poll();
},
/* Subscribe to a channel
*/
subscribe: function(channel, func, lastId) {
callbacks.push({
channel: channel,
func: func,
last_id: lastId
});
if (this.longPoll) {
return this.longPoll.abort();
}
},
/* Unsubscribe from a channel
*/
unsubscribe: function(channel) {
/* TODO proper globbing
*/
var glob;
if (channel.endsWith("*")) {
channel = channel.substr(0, channel.length - 1);
glob = true;
}
callbacks = callbacks.filter(function(callback) {
if (glob) {
return callback.channel.substr(0, channel.length) !== channel;
} else {
return callback.channel !== channel;
} }
}); });
if (this.longPoll) { };
return this.longPoll.abort(); poll();
} },
}
};
})();
}).call(this); // Subscribe to a channel
subscribe: function(channel, func, lastId) {
callbacks.push({
channel: channel,
func: func,
last_id: lastId
});
if (this.longPoll) {
return this.longPoll.abort();
}
},
// Unsubscribe from a channel
unsubscribe: function(channel) {
// TODO proper globbing
var glob;
if (channel.endsWith("*")) {
channel = channel.substr(0, channel.length - 1);
glob = true;
}
callbacks = callbacks.filter(function(callback) {
if (glob) {
return callback.channel.substr(0, channel.length) !== channel;
} else {
return callback.channel !== channel;
}
});
if (this.longPoll) {
return this.longPoll.abort();
}
}
};
})();

View File

@ -0,0 +1,89 @@
/**
A helper for looking up oneboxes and displaying them
For now it only stores in a var, in future we can change it so it uses localStorage.
@class Notification
@extends Discourse.Model
@namespace Discourse
@module Discourse
**/
Discourse.Onebox = (function() {
var cache, load, localCache, lookup, lookupCache;
localCache = {};
cache = function(url, contents) {
localCache[url] = contents;
return null;
};
lookupCache = function(url) {
var cached;
cached = localCache[url];
if (cached && cached.then) {
return null;
} else {
return cached;
}
};
lookup = function(url, refresh, callback) {
var cached;
cached = localCache[url];
if (refresh && cached && !cached.then) {
cached = null;
}
if (cached) {
if (cached.then) {
cached.then(callback(lookupCache(url)));
} else {
callback(cached);
}
return false;
} else {
cache(url, jQuery.get("/onebox", {
url: url,
refresh: refresh
}, function(html) {
cache(url, html);
return callback(html);
}));
return true;
}
};
load = function(e, refresh) {
var $elem, loading, url;
if (!refresh) refresh = false;
url = e.href;
$elem = $(e);
if ($elem.data('onebox-loaded')) {
return;
}
loading = lookup(url, refresh, function(html) {
$elem.removeClass('loading-onebox');
$elem.data('onebox-loaded');
if (!html) {
return;
}
if (html.trim().length === 0) {
return;
}
return $elem.replaceWith(html);
});
if (loading) {
return $elem.addClass('loading-onebox');
}
};
return {
load: load,
lookup: lookup,
lookupCache: lookupCache
};
})();

View File

@ -1,38 +0,0 @@
/*global Markdown:true*/
(function() {
window.Discourse.PagedownEditor = Ember.ContainerView.extend({
elementId: 'pagedown-editor',
init: function() {
this._super();
/* Add a button bar
*/
this.pushObject(Em.View.create({
elementId: 'wmd-button-bar'
}));
this.pushObject(Em.TextArea.create({
valueBinding: 'parentView.value',
elementId: 'wmd-input'
}));
return this.pushObject(Em.View.createWithMixins(Discourse.Presence, {
elementId: 'wmd-preview',
classNameBindings: [':preview', 'hidden'],
hidden: (function() {
return this.blank('parentView.value');
}).property('parentView.value')
}));
},
didInsertElement: function() {
var $wmdInput;
$wmdInput = jQuery('#wmd-input');
$wmdInput.data('init', true);
this.editor = new Markdown.Editor(Discourse.Utilities.markdownConverter({
sanitize: true
}));
return this.editor.run();
}
});
}).call(this);

View File

@ -5,27 +5,26 @@
* *
* Examples: * Examples:
* *
*
someFunction = window.probes.measure(someFunction, { * someFunction = window.probes.measure(someFunction, {
name: "somename" // or function(args) { return "name"; }, * name: "somename" // or function(args) { return "name"; },
before: function(data, owner, args) { * before: function(data, owner, args) {
// if owner is true, we are not in a recursive function call. * // if owner is true, we are not in a recursive function call.
// * //
// data contains the bucker of data already measuer * // data contains the bucker of data already measuer
// data.count >= 0 * // data.count >= 0
// data.time is the total time measured till now * // data.time is the total time measured till now
// * //
// arguments contains the original arguments sent to the function * // arguments contains the original arguments sent to the function
}, * },
after: function(data, owner, args) { * after: function(data, owner, args) {
// same format as before * // same format as before
} * }
}); * });
*
*
// minimal * // minimal
someFunction = window.probes.measure(someFunction, "someFunction"); * someFunction = window.probes.measure(someFunction, "someFunction");
* *
* *
* */ * */

View File

@ -1,92 +0,0 @@
// Sam: I wrote this but it is totally unsafe so I ported Google Cajole
// Thing is Cajole is old and complex (albeit super duper fast)
//
// I would like this ported to: https://github.com/tautologistics/node-htmlparser , perf tested
// and move off cajole
//
// See also: http://stackoverflow.com/questions/14971083/is-jquerys-safe-from-xss
//
// (function( $ ) {
//
// var elements = ["a", "abbr", "aside", "b", "bdo", "blockquote", "br",
// "caption", "cite", "code", "col", "colgroup", "dd", "div",
// "del", "dfn", "dl", "dt", "em", "hr", "figcaption", "figure",
// "h1", "h2", "h3", "h4", "h5", "h6", "hgroup", "i", "img", "ins",
// "kbd", "li", "mark", "ol", "p", "pre", "q", "rp", "rt", "ruby",
// "s", "samp", "small", "span", "strike", "strong", "sub", "sup",
// "table", "tbody", "td", "tfoot", "th", "thead", "time", "tr", "u",
// "ul", "var", "wbr"];
//
// var attributes = {
// 'all' : ['dir', 'lang', 'title', 'class'],
// 'aside' : ['data-post', 'data-full', 'data-topic'],
// 'a' : ['href'],
// 'blockquote' : ['cite'],
// 'col' : ['span', 'width'],
// 'colgroup' : ['span', 'width'],
// 'del' : ['cite', 'datetime'],
// 'img' : ['align', 'alt', 'height', 'src', 'width'],
// 'ins' : ['cite', 'datetime'],
// 'ol' : ['start', 'reversed', 'type'],
// 'q' : ['cite'],
// 'span' : ['style'],
// 'table' : ['summary', 'width', 'style', 'cellpadding', 'cellspacing'],
// 'td' : ['abbr', 'axis', 'colspan', 'rowspan', 'width', 'style'],
// 'th' : ['abbr', 'axis', 'colspan', 'rowspan', 'scope', 'width', 'style'],
// 'time' : ['datetime', 'pubdate'],
// 'ul' : ['type']
//
// };
//
// var elementMap = {};
// jQuery.each(elements, function(idx,e){
// elementMap[e] = true;
// });
//
// var scrubAttributes = function(e){
// jQuery.each(e.attributes, function(idx, attr){
//
// if(jQuery.inArray(attr.name, attributes.all) === -1 &&
// jQuery.inArray(attr.name, attributes[e.tagName.toLowerCase()]) === -1) {
// e.removeAttribute(attr.name);
// }
// });
// return(e);
// };
//
// var scrubNode = function(e){
// if (!e.tagName) { return(e); }
// if(elementMap[e.tagName.toLowerCase()]){
// return scrubAttributes(e);
// }
// else
// {
// return null;
// }
// };
//
// var scrubTree = function(e) {
// if (!e) { return; }
//
// var clean = scrubNode(e);
// if(!clean){
// e.parentNode.removeChild(e);
// }
// else {
// jQuery.each(clean.children, function(idx, inner){
// scrubTree(inner);
// });
// }
// };
//
// $.fn.sanitize = function() {
// clean = this.filter(function(){
// return scrubNode(this);
// }).each(function(){
// scrubTree(this);
// });
//
// return clean;
// };
// })( jQuery );

View File

@ -1,169 +1,159 @@
/**
We use this class to track how long posts in a topic are on the screen.
/* We use this class to track how long posts in a topic are on the screen. @class ScreenTrack
*/ @extends Ember.Object
@namespace Discourse
@module Discourse
**/
Discourse.ScreenTrack = Ember.Object.extend({
// Don't send events if we haven't scrolled in a long time
PAUSE_UNLESS_SCROLLED: 1000 * 60 * 3,
/* This could be a potentially awesome metric to keep track of. // After 6 minutes stop tracking read position on post
*/ MAX_TRACKING_TIME: 1000 * 60 * 6,
totalTimings: {},
(function() { // Elements to track
timings: {},
topicTime: 0,
cancelled: false,
window.Discourse.ScreenTrack = Ember.Object.extend({ track: function(elementId, postNumber) {
/* Don't send events if we haven't scrolled in a long time this.timings["#" + elementId] = {
*/ time: 0,
postNumber: postNumber
};
},
PAUSE_UNLESS_SCROLLED: 1000 * 60 * 3, guessedSeen: function(postNumber) {
/* After 6 minutes stop tracking read position on post if (postNumber > (this.highestSeen || 0)) {
*/ this.highestSeen = postNumber;
MAX_TRACKING_TIME: 1000 * 60 * 6,
totalTimings: {},
/* Elements to track
*/
timings: {},
topicTime: 0,
cancelled: false,
track: function(elementId, postNumber) {
this.timings["#" + elementId] = {
time: 0,
postNumber: postNumber
};
},
guessedSeen: function(postNumber) {
if (postNumber > (this.highestSeen || 0)) {
this.highestSeen = postNumber;
}
},
/* Reset our timers
*/
reset: function() {
this.lastTick = new Date().getTime();
this.lastFlush = 0;
this.cancelled = false;
},
/* Start tracking
*/
start: function() {
var _this = this;
this.reset();
this.lastScrolled = new Date().getTime();
this.interval = setInterval(function() {
return _this.tick();
}, 1000);
},
/* Cancel and eject any tracking we have buffered
*/
cancel: function() {
this.cancelled = true;
this.timings = {};
this.topicTime = 0;
clearInterval(this.interval);
this.interval = null;
},
/* Stop tracking and flush buffered read records
*/
stop: function() {
clearInterval(this.interval);
this.interval = null;
return this.flush();
},
scrolled: function() {
this.lastScrolled = new Date().getTime();
},
flush: function() {
var highestSeenByTopic, newTimings, topicId,
_this = this;
if (this.cancelled) {
return;
}
/* We don't log anything unless we're logged in
*/
if (!Discourse.get('currentUser')) {
return;
}
newTimings = {};
Object.values(this.timings, function(timing) {
if (!_this.totalTimings[timing.postNumber])
_this.totalTimings[timing.postNumber] = 0;
if (timing.time > 0 && _this.totalTimings[timing.postNumber] < _this.MAX_TRACKING_TIME) {
_this.totalTimings[timing.postNumber] += timing.time;
newTimings[timing.postNumber] = timing.time;
}
timing.time = 0;
});
topicId = this.get('topic_id');
highestSeenByTopic = Discourse.get('highestSeenByTopic');
if ((highestSeenByTopic[topicId] || 0) < this.highestSeen) {
highestSeenByTopic[topicId] = this.highestSeen;
}
if (!Object.isEmpty(newTimings)) {
jQuery.ajax('/topics/timings', {
data: {
timings: newTimings,
topic_time: this.topicTime,
highest_seen: this.highestSeen,
topic_id: topicId
},
cache: false,
type: 'POST',
headers: {
'X-SILENCE-LOGGER': 'true'
}
});
this.topicTime = 0;
}
this.lastFlush = 0;
},
tick: function() {
/* If the user hasn't scrolled the browser in a long time, stop tracking time read
*/
var diff, docViewBottom, docViewTop, sinceScrolled,
_this = this;
sinceScrolled = new Date().getTime() - this.lastScrolled;
if (sinceScrolled > this.PAUSE_UNLESS_SCROLLED) {
this.reset();
return;
}
diff = new Date().getTime() - this.lastTick;
this.lastFlush += diff;
this.lastTick = new Date().getTime();
if (this.lastFlush > (Discourse.SiteSettings.flush_timings_secs * 1000)) {
this.flush();
}
/* Don't track timings if we're not in focus
*/
if (!Discourse.get("hasFocus")) {
return;
}
this.topicTime += diff;
docViewTop = jQuery(window).scrollTop() + jQuery('header').height();
docViewBottom = docViewTop + jQuery(window).height();
return Object.keys(this.timings, function(id) {
var $element, elemBottom, elemTop, timing;
$element = jQuery(id);
if ($element.length === 1) {
elemTop = $element.offset().top;
elemBottom = elemTop + $element.height();
/* If part of the element is on the screen, increase the counter
*/
if (((docViewTop <= elemTop && elemTop <= docViewBottom)) || ((docViewTop <= elemBottom && elemBottom <= docViewBottom))) {
timing = _this.timings[id];
timing.time = timing.time + diff;
}
}
});
} }
}); },
// Reset our timers
reset: function() {
this.lastTick = new Date().getTime();
this.lastFlush = 0;
this.cancelled = false;
},
// Start tracking
start: function() {
var _this = this;
this.reset();
this.lastScrolled = new Date().getTime();
this.interval = setInterval(function() {
return _this.tick();
}, 1000);
},
// Cancel and eject any tracking we have buffered
cancel: function() {
this.cancelled = true;
this.timings = {};
this.topicTime = 0;
clearInterval(this.interval);
this.interval = null;
},
// Stop tracking and flush buffered read records
stop: function() {
clearInterval(this.interval);
this.interval = null;
return this.flush();
},
scrolled: function() {
this.lastScrolled = new Date().getTime();
},
flush: function() {
var highestSeenByTopic, newTimings, topicId,
_this = this;
if (this.cancelled) {
return;
}
// We don't log anything unless we're logged in
if (!Discourse.get('currentUser')) {
return;
}
newTimings = {};
Object.values(this.timings, function(timing) {
if (!_this.totalTimings[timing.postNumber])
_this.totalTimings[timing.postNumber] = 0;
if (timing.time > 0 && _this.totalTimings[timing.postNumber] < _this.MAX_TRACKING_TIME) {
_this.totalTimings[timing.postNumber] += timing.time;
newTimings[timing.postNumber] = timing.time;
}
timing.time = 0;
});
topicId = this.get('topic_id');
highestSeenByTopic = Discourse.get('highestSeenByTopic');
if ((highestSeenByTopic[topicId] || 0) < this.highestSeen) {
highestSeenByTopic[topicId] = this.highestSeen;
}
if (!Object.isEmpty(newTimings)) {
jQuery.ajax('/topics/timings', {
data: {
timings: newTimings,
topic_time: this.topicTime,
highest_seen: this.highestSeen,
topic_id: topicId
},
cache: false,
type: 'POST',
headers: {
'X-SILENCE-LOGGER': 'true'
}
});
this.topicTime = 0;
}
this.lastFlush = 0;
},
tick: function() {
// If the user hasn't scrolled the browser in a long time, stop tracking time read
var diff, docViewBottom, docViewTop, sinceScrolled,
_this = this;
sinceScrolled = new Date().getTime() - this.lastScrolled;
if (sinceScrolled > this.PAUSE_UNLESS_SCROLLED) {
this.reset();
return;
}
diff = new Date().getTime() - this.lastTick;
this.lastFlush += diff;
this.lastTick = new Date().getTime();
if (this.lastFlush > (Discourse.SiteSettings.flush_timings_secs * 1000)) {
this.flush();
}
// Don't track timings if we're not in focus
if (!Discourse.get("hasFocus")) return;
this.topicTime += diff;
docViewTop = $(window).scrollTop() + $('header').height();
docViewBottom = docViewTop + $(window).height();
return Object.keys(this.timings, function(id) {
var $element, elemBottom, elemTop, timing;
$element = $(id);
if ($element.length === 1) {
elemTop = $element.offset().top;
elemBottom = elemTop + $element.height();
// If part of the element is on the screen, increase the counter
if (((docViewTop <= elemTop && elemTop <= docViewBottom)) || ((docViewTop <= elemBottom && elemBottom <= docViewBottom))) {
timing = _this.timings[id];
timing.time = timing.time + diff;
}
}
});
}
});
}).call(this);

View File

@ -1,18 +1,28 @@
/*global hljs:true */ /*global hljs:true */
/* Helper object for syntax highlighting. Uses highlight.js which is loaded /**
on demand. */ Helper object for syntax highlighting. Uses highlight.js which is loaded on demand.
(function() {
window.Discourse.SyntaxHighlighting = { @class SyntaxHighlighting
apply: function($elem) { @namespace Discourse
var _this = this; @module Discourse
return jQuery('pre code[class]', $elem).each(function(i, e) { **/
return $LAB.script("/javascripts/highlight-handlebars.pack.js").wait(function() { Discourse.SyntaxHighlighting = {
return hljs.highlightBlock(e);
}); /**
Apply syntax highlighting to a jQuery element
@method apply
@param {jQuery.selector} $elem The element we want to apply our highlighting to
**/
apply: function($elem) {
var _this = this;
return $('pre code[class]', $elem).each(function(i, e) {
return $LAB.script("/javascripts/highlight-handlebars.pack.js").wait(function() {
return hljs.highlightBlock(e);
}); });
} });
}; }
};
}).call(this);

View File

@ -1,45 +1,43 @@
/**
CSS transitions are a PITA, often we need to queue some js after a transition, this helper ensures
it happens after the transition.
/* CSS transitions are a PITA, often we need to queue some js after a transition, this helper ensures SO: http://stackoverflow.com/questions/9943435/css3-animation-end-techniques
*/
/* it happens after the transition @class TransitionHelper
*/ @namespace Discourse
@module Discourse
**/
var dummy, eventNameHash, transitionEnd, _getTransitionEndEventName;
/* SO: http://stackoverflow.com/questions/9943435/css3-animation-end-techniques dummy = document.createElement("div");
*/
eventNameHash = {
webkit: "webkitTransitionEnd",
Moz: "transitionend",
O: "oTransitionEnd",
ms: "MSTransitionEnd"
};
(function() { _getTransitionEndEventName = function() {
var dummy, eventNameHash, transitionEnd, _getTransitionEndEventName; var retValue;
retValue = "transitionend";
dummy = document.createElement("div"); Object.keys(eventNameHash).some(function(vendor) {
if (vendor + "TransitionProperty" in dummy.style) {
eventNameHash = { retValue = eventNameHash[vendor];
webkit: "webkitTransitionEnd", return true;
Moz: "transitionend",
O: "oTransitionEnd",
ms: "MSTransitionEnd"
};
_getTransitionEndEventName = function() {
var retValue;
retValue = "transitionend";
Object.keys(eventNameHash).some(function(vendor) {
if (vendor + "TransitionProperty" in dummy.style) {
retValue = eventNameHash[vendor];
return true;
}
});
return retValue;
};
transitionEnd = _getTransitionEndEventName();
window.Discourse.TransitionHelper = {
after: function(element, callback) {
return jQuery(element).on(transitionEnd, callback);
} }
}; });
return retValue;
};
transitionEnd = _getTransitionEndEventName();
window.Discourse.TransitionHelper = {
after: function(element, callback) {
return $(element).on(transitionEnd, callback);
}
};
}).call(this);

View File

@ -1,76 +1,81 @@
(function() { /**
var cache, cacheTime, cacheTopicId, debouncedSearch, doSearch; Helper for searching for Users
cache = {}; @class UserSearch
@namespace Discourse
@module Discourse
**/
var cache, cacheTime, cacheTopicId, debouncedSearch, doSearch;
cacheTopicId = null; cache = {};
cacheTime = null; cacheTopicId = null;
doSearch = function(term, topicId, success) { cacheTime = null;
return jQuery.ajax({
url: '/users/search/users',
dataType: 'JSON',
data: {
term: term,
topic_id: topicId
},
success: function(r) {
cache[term] = r;
cacheTime = new Date();
return success(r);
}
});
};
debouncedSearch = Discourse.debounce(doSearch, 200); doSearch = function(term, topicId, success) {
return jQuery.ajax({
url: '/users/search/users',
dataType: 'JSON',
data: {
term: term,
topic_id: topicId
},
success: function(r) {
cache[term] = r;
cacheTime = new Date();
return success(r);
}
});
};
window.Discourse.UserSearch = { debouncedSearch = Discourse.debounce(doSearch, 200);
search: function(options) {
var callback, exclude, limit, success, term, topicId;
term = options.term || "";
callback = options.callback;
exclude = options.exclude || [];
topicId = options.topicId;
limit = options.limit || 5;
if (!callback) {
throw "missing callback";
}
/*TODO site setting for allowed regex in username ?
*/
if (term.match(/[^a-zA-Z0-9\_\.]/)) { Discourse.UserSearch = {
callback([]); search: function(options) {
return true; var callback, exclude, limit, success, term, topicId;
} term = options.term || "";
if ((new Date() - cacheTime) > 30000) { callback = options.callback;
cache = {}; exclude = options.exclude || [];
} topicId = options.topicId;
if (cacheTopicId !== topicId) { limit = options.limit || 5;
cache = {}; if (!callback) {
} throw "missing callback";
cacheTopicId = topicId; }
success = function(r) {
var result; // TODO site setting for allowed regex in username
result = []; if (term.match(/[^a-zA-Z0-9\_\.]/)) {
r.users.each(function(u) { callback([]);
if (exclude.indexOf(u.username) === -1) {
result.push(u);
}
if (result.length > limit) {
return false;
}
return true;
});
return callback(result);
};
if (cache[term]) {
success(cache[term]);
} else {
debouncedSearch(term, topicId, success);
}
return true; return true;
} }
}; if ((new Date() - cacheTime) > 30000) {
cache = {};
}
if (cacheTopicId !== topicId) {
cache = {};
}
cacheTopicId = topicId;
success = function(r) {
var result;
result = [];
r.users.each(function(u) {
if (exclude.indexOf(u.username) === -1) {
result.push(u);
}
if (result.length > limit) {
return false;
}
return true;
});
return callback(result);
};
if (cache[term]) {
success(cache[term]);
} else {
debouncedSearch(term, topicId, success);
}
return true;
}
};
}).call(this);

View File

@ -1,277 +1,266 @@
/*global sanitizeHtml:true Markdown:true */ /*global sanitizeHtml:true Markdown:true */
(function() { /**
var baseUrl, site; General utility functions
baseUrl = null; @class Utilities
@namespace Discourse
@module Discourse
**/
Discourse.Utilities = {
site = null; translateSize: function(size) {
switch (size) {
case 'tiny':
size = 20;
break;
case 'small':
size = 25;
break;
case 'medium':
size = 32;
break;
case 'large':
size = 45;
}
return size;
},
Discourse.Utilities = { categoryUrlId: function(category) {
translateSize: function(size) { var id, slug;
switch (size) { if (!category) {
case 'tiny': return "";
size = 20; }
break; id = Em.get(category, 'id');
case 'small': slug = Em.get(category, 'slug');
size = 25; if ((!slug) || slug.isBlank()) {
break; return "" + id + "-category";
case 'medium': }
size = 32; return slug;
break; },
case 'large':
size = 45;
}
return size;
},
categoryUrlId: function(category) {
var id, slug;
if (!category) {
return "";
}
id = Em.get(category, 'id');
slug = Em.get(category, 'slug');
if ((!slug) || slug.isBlank()) {
return "" + id + "-category";
}
return slug;
},
/* Create a badge like category link
*/
categoryLink: function(category) { // Create a badge like category link
var color, name, description, result; categoryLink: function(category) {
if (!category) return ""; var color, name, description, result;
if (!category) return "";
color = Em.get(category, 'color'); color = Em.get(category, 'color');
name = Em.get(category, 'name'); name = Em.get(category, 'name');
description = Em.get(category, 'description'); description = Em.get(category, 'description');
// Build the HTML link // Build the HTML link
result = "<a href=\"/category/" + result = "<a href=\"/category/" + this.categoryUrlId(category) + "\" class=\"badge-category\" ";
this.categoryUrlId(category) +
"\" class=\"badge-category excerptable\" data-excerpt-size=\"medium\" ";
// Add description if we have it // Add description if we have it
if (description) result += "title=\"" + description + "\" "; if (description) result += "title=\"" + description + "\" ";
return result + "style=\"background-color: #" + color + "\">" + name + "</a>"; return result + "style=\"background-color: #" + color + "\">" + name + "</a>";
}, },
avatarUrl: function(username, size, template) {
var rawSize;
if (!username) {
return "";
}
size = Discourse.Utilities.translateSize(size);
rawSize = (size * (window.devicePixelRatio || 1)).toFixed();
if (template) {
return template.replace(/\{size\}/g, rawSize);
}
return "/users/" + (username.toLowerCase()) + "/avatar/" + rawSize + "?__ws=" + (encodeURIComponent(Discourse.BaseUrl || ""));
},
avatarImg: function(options) {
var extraClasses, size, title, url;
size = Discourse.Utilities.translateSize(options.size);
title = options.title || "";
extraClasses = options.extraClasses || "";
url = Discourse.Utilities.avatarUrl(options.username, options.size, options.avatarTemplate);
return "<img width='" + size + "' height='" + size + "' src='" + url + "' class='avatar " +
(extraClasses || "") + "' title='" + (Handlebars.Utils.escapeExpression(title || "")) + "'>";
},
postUrl: function(slug, topicId, postNumber) {
var url;
url = "/t/";
if (slug) {
url += slug + "/";
}
url += topicId;
if (postNumber > 1) {
url += "/" + postNumber;
}
return url;
},
emailValid: function(email) {
/* see: http://stackoverflow.com/questions/46155/validate-email-address-in-javascript
*/
var re; avatarUrl: function(username, size, template) {
re = /^[a-zA-Z0-9!#$%&'*+\/=?\^_`{|}~\-]+(?:\.[a-zA-Z0-9!#$%&'\*+\/=?\^_`{|}~\-]+)*@(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]*[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9\-]*[a-zA-Z0-9])?$/; var rawSize;
return re.test(email); if (!username) {
}, return "";
selectedText: function() { }
var t; size = Discourse.Utilities.translateSize(size);
t = ''; rawSize = (size * (window.devicePixelRatio || 1)).toFixed();
if (window.getSelection) { if (template) {
t = window.getSelection().toString(); return template.replace(/\{size\}/g, rawSize);
} else if (document.getSelection) { }
t = document.getSelection().toString(); return "/users/" + (username.toLowerCase()) + "/avatar/" + rawSize + "?__ws=" + (encodeURIComponent(Discourse.BaseUrl || ""));
} else if (document.selection) { },
t = document.selection.createRange().text;
}
return String(t).trim();
},
/* Determine the position of the caret in an element
*/
caretPosition: function(el) { avatarImg: function(options) {
var r, rc, re; var extraClasses, size, title, url;
if (el.selectionStart) { size = Discourse.Utilities.translateSize(options.size);
return el.selectionStart; title = options.title || "";
} extraClasses = options.extraClasses || "";
if (document.selection) { url = Discourse.Utilities.avatarUrl(options.username, options.size, options.avatarTemplate);
el.focus(); return "<img width='" + size + "' height='" + size + "' src='" + url + "' class='avatar " +
r = document.selection.createRange(); (extraClasses || "") + "' title='" + (Handlebars.Utils.escapeExpression(title || "")) + "'>";
if (!r) return 0; },
re = el.createTextRange();
rc = re.duplicate();
re.moveToBookmark(r.getBookmark());
rc.setEndPoint('EndToStart', re);
return rc.text.length;
}
return 0;
},
/* Set the caret's position
*/
setCaretPosition: function(ctrl, pos) { postUrl: function(slug, topicId, postNumber) {
var range; var url;
if (ctrl.setSelectionRange) { url = "/t/";
ctrl.focus(); if (slug) {
ctrl.setSelectionRange(pos, pos); url += slug + "/";
return; }
} url += topicId;
if (ctrl.createTextRange) { if (postNumber > 1) {
range = ctrl.createTextRange(); url += "/" + postNumber;
range.collapse(true); }
range.moveEnd('character', pos); return url;
range.moveStart('character', pos); },
return range.select();
}
},
markdownConverter: function(opts) {
var converter, mentionLookup,
_this = this;
converter = new Markdown.Converter();
if (opts) {
mentionLookup = opts.mentionLookup;
}
mentionLookup = mentionLookup || Discourse.Mention.lookupCache;
/* Before cooking callbacks
*/
converter.hooks.chain("preConversion", function(text) { emailValid: function(email) {
_this.trigger('beforeCook', { // see: http://stackoverflow.com/questions/46155/validate-email-address-in-javascript
detail: text, var re;
opts: opts re = /^[a-zA-Z0-9!#$%&'*+\/=?\^_`{|}~\-]+(?:\.[a-zA-Z0-9!#$%&'\*+\/=?\^_`{|}~\-]+)*@(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]*[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9\-]*[a-zA-Z0-9])?$/;
}); return re.test(email);
return _this.textResult || text; },
selectedText: function() {
var t;
t = '';
if (window.getSelection) {
t = window.getSelection().toString();
} else if (document.getSelection) {
t = document.getSelection().toString();
} else if (document.selection) {
t = document.selection.createRange().text;
}
return String(t).trim();
},
// Determine the position of the caret in an element
caretPosition: function(el) {
var r, rc, re;
if (el.selectionStart) {
return el.selectionStart;
}
if (document.selection) {
el.focus();
r = document.selection.createRange();
if (!r) return 0;
re = el.createTextRange();
rc = re.duplicate();
re.moveToBookmark(r.getBookmark());
rc.setEndPoint('EndToStart', re);
return rc.text.length;
}
return 0;
},
// Set the caret's position
setCaretPosition: function(ctrl, pos) {
var range;
if (ctrl.setSelectionRange) {
ctrl.focus();
ctrl.setSelectionRange(pos, pos);
return;
}
if (ctrl.createTextRange) {
range = ctrl.createTextRange();
range.collapse(true);
range.moveEnd('character', pos);
range.moveStart('character', pos);
return range.select();
}
},
markdownConverter: function(opts) {
var converter, mentionLookup,
_this = this;
converter = new Markdown.Converter();
if (opts) {
mentionLookup = opts.mentionLookup;
}
mentionLookup = mentionLookup || Discourse.Mention.lookupCache;
// Before cooking callbacks
converter.hooks.chain("preConversion", function(text) {
_this.trigger('beforeCook', {
detail: text,
opts: opts
}); });
/* Support autolinking of www.something.com return _this.textResult || text;
*/ });
// Support autolinking of www.something.com
converter.hooks.chain("preConversion", function(text) {
return text.replace(/(^|[\s\n])(www\.[a-z\.\-\_\(\)\/\?\=\%0-9]+)/gim, function(full, _, rest) {
return " <a href=\"http://" + rest + "\">" + rest + "</a>";
});
});
// newline prediction in trivial cases
if (!Discourse.SiteSettings.traditional_markdown_linebreaks) {
converter.hooks.chain("preConversion", function(text) { converter.hooks.chain("preConversion", function(text) {
return text.replace(/(^|[\s\n])(www\.[a-z\.\-\_\(\)\/\?\=\%0-9]+)/gim, function(full, _, rest) { return text.replace(/(^[\w<][^\n]*\n+)/gim, function(t) {
return " <a href=\"http://" + rest + "\">" + rest + "</a>"; if (t.match(/\n{2}/gim)) {
return t;
}
return t.replace("\n", " \n");
}); });
}); });
/* newline prediction in trivial cases }
*/
if (!Discourse.SiteSettings.traditional_markdown_linebreaks) { // github style fenced code
converter.hooks.chain("preConversion", function(text) { converter.hooks.chain("preConversion", function(text) {
return text.replace(/(^[\w<][^\n]*\n+)/gim, function(t) { return text.replace(/^`{3}(?:(.*$)\n)?([\s\S]*?)^`{3}/gm, function(wholeMatch, m1, m2) {
if (t.match(/\n{2}/gim)) { var escaped;
return t; escaped = Handlebars.Utils.escapeExpression(m2);
} return "<pre><code class='" + (m1 || 'lang-auto') + "'>" + escaped + "</code></pre>";
return t.replace("\n", " \n");
});
});
}
/* github style fenced code
*/
converter.hooks.chain("preConversion", function(text) {
return text.replace(/^`{3}(?:(.*$)\n)?([\s\S]*?)^`{3}/gm, function(wholeMatch, m1, m2) {
var escaped;
escaped = Handlebars.Utils.escapeExpression(m2);
return "<pre><code class='" + (m1 || 'lang-auto') + "'>" + escaped + "</code></pre>";
});
}); });
});
converter.hooks.chain("postConversion", function(text) {
if (!text) return "";
// don't to mention voodoo in pres
text = text.replace(/<pre>([\s\S]*@[\s\S]*)<\/pre>/gi, function(wholeMatch, inner) {
return "<pre>" + (inner.replace(/@/g, '&#64;')) + "</pre>";
});
// Add @mentions of names
text = text.replace(/([\s\t>,:'|";\]])(@[A-Za-z0-9_-|\.]*[A-Za-z0-9_-|]+)(?=[\s\t<\!:|;',"\?\.])/g, function(x, pre, name) {
if (mentionLookup(name.substr(1))) {
return "" + pre + "<a href='/users/" + (name.substr(1).toLowerCase()) + "' class='mention'>" + name + "</a>";
} else {
return "" + pre + "<span class='mention'>" + name + "</span>";
}
});
// a primitive attempt at oneboxing, this regex gives me much eye sores
text = text.replace(/(<li>)?((<p>|<br>)[\s\n\r]*)(<a href=["]([^"]+)[^>]*)>([^<]+<\/a>[\s\n\r]*(?=<\/p>|<br>))/gi, function() {
// We don't onebox items in a list
var onebox, url;
if (arguments[1]) {
return arguments[0];
}
url = arguments[5];
if (Discourse && Discourse.Onebox) {
onebox = Discourse.Onebox.lookupCache(url);
}
if (onebox && !onebox.isBlank()) {
return arguments[2] + onebox;
} else {
return arguments[2] + arguments[4] + " class=\"onebox\" target=\"_blank\">" + arguments[6];
}
});
return(text);
});
converter.hooks.chain("postConversion", function(text) {
return Discourse.BBCode.format(text, opts);
});
if (opts.sanitize) {
converter.hooks.chain("postConversion", function(text) { converter.hooks.chain("postConversion", function(text) {
if (!text) { if (!window.sanitizeHtml) {
return ""; return "";
} }
/* don't to mention voodoo in pres return sanitizeHtml(text);
*/
text = text.replace(/<pre>([\s\S]*@[\s\S]*)<\/pre>/gi, function(wholeMatch, inner) {
return "<pre>" + (inner.replace(/@/g, '&#64;')) + "</pre>";
});
/* Add @mentions of names
*/
text = text.replace(/([\s\t>,:'|";\]])(@[A-Za-z0-9_-|\.]*[A-Za-z0-9_-|]+)(?=[\s\t<\!:|;',"\?\.])/g, function(x, pre, name) {
if (mentionLookup(name.substr(1))) {
return "" + pre + "<a href='/users/" + (name.substr(1).toLowerCase()) + "' class='mention'>" + name + "</a>";
} else {
return "" + pre + "<span class='mention'>" + name + "</span>";
}
});
/* a primitive attempt at oneboxing, this regex gives me much eye sores
*/
text = text.replace(/(<li>)?((<p>|<br>)[\s\n\r]*)(<a href=["]([^"]+)[^>]*)>([^<]+<\/a>[\s\n\r]*(?=<\/p>|<br>))/gi, function() {
/* We don't onebox items in a list
*/
var onebox, url;
if (arguments[1]) {
return arguments[0];
}
url = arguments[5];
if (Discourse && Discourse.Onebox) {
onebox = Discourse.Onebox.lookupCache(url);
}
if (onebox && !onebox.isBlank()) {
return arguments[2] + onebox;
} else {
return arguments[2] + arguments[4] + " class=\"onebox\" target=\"_blank\">" + arguments[6];
}
});
return(text);
}); });
converter.hooks.chain("postConversion", function(text) {
return Discourse.BBCode.format(text, opts);
});
if (opts.sanitize) {
converter.hooks.chain("postConversion", function(text) {
if (!window.sanitizeHtml) {
return "";
}
return sanitizeHtml(text);
});
}
return converter;
},
/* Takes raw input and cooks it to display nicely (mostly markdown)
*/
cook: function(raw, opts) {
if (!opts) opts = {};
// Make sure we've got a string
if (!raw) return "";
if (raw.length === 0) return "";
this.converter = this.markdownConverter(opts);
return this.converter.makeHtml(raw);
} }
}; return converter;
},
RSVP.EventTarget.mixin(Discourse.Utilities); // Takes raw input and cooks it to display nicely (mostly markdown)
cook: function(raw, opts) {
if (!opts) opts = {};
}).call(this); // Make sure we've got a string
if (!raw) return "";
if (raw.length === 0) return "";
this.converter = this.markdownConverter(opts);
return this.converter.makeHtml(raw);
}
};
RSVP.EventTarget.mixin(Discourse.Utilities);

View File

@ -1,7 +1,16 @@
/*global _gaq:true */ /*global _gaq:true */
window.Discourse.ApplicationController = Ember.Controller.extend({ /**
The base controller for all things Discourse
@class ApplicationController
@extends Discourse.Controller
@namespace Discourse
@module Discourse
**/
Discourse.ApplicationController = Discourse.Controller.extend({
needs: ['modal'], needs: ['modal'],
showLogin: function() { showLogin: function() {
var _ref; var _ref;
return (_ref = this.get('controllers.modal')) ? _ref.show(Discourse.LoginView.create()) : void 0; return (_ref = this.get('controllers.modal')) ? _ref.show(Discourse.LoginView.create()) : void 0;
@ -18,5 +27,5 @@ window.Discourse.ApplicationController = Ember.Controller.extend({
this.afterFirstHit = true; this.afterFirstHit = true;
} }
}.observes('currentPath') }.observes('currentPath')
}); });

View File

@ -1,261 +1,271 @@
(function() { /**
This controller supports composing new posts and topics.
window.Discourse.ComposerController = Ember.Controller.extend(Discourse.Presence, { @class ComposerController
needs: ['modal', 'topic'], @extends Discourse.Controller
hasReply: false, @namespace Discourse
togglePreview: function() { @module Discourse
return this.get('content').togglePreview(); **/
}, Discourse.ComposerController = Discourse.Controller.extend({
/* Import a quote from the post needs: ['modal', 'topic'],
*/ hasReply: false,
importQuote: function() { togglePreview: function() {
return this.get('content').importQuote(); return this.get('content').togglePreview();
}, },
appendText: function(text) {
var c; // Import a quote from the post
c = this.get('content'); importQuote: function() {
if (c) { return this.get('content').importQuote();
return c.appendText(text); },
}
}, appendText: function(text) {
save: function() { var c;
var composer, c = this.get('content');
_this = this; if (c) {
composer = this.get('content'); return c.appendText(text);
composer.set('disableDrafts', true); }
return composer.save({ },
imageSizes: this.get('view').imageSizes()
}).then(function(opts) { save: function() {
opts = opts || {}; var composer,
_this.close(); _this = this;
if (composer.get('creatingTopic')) { composer = this.get('content');
Discourse.set('currentUser.topic_count', Discourse.get('currentUser.topic_count') + 1); composer.set('disableDrafts', true);
} else { return composer.save({
Discourse.set('currentUser.reply_count', Discourse.get('currentUser.reply_count') + 1); imageSizes: this.get('view').imageSizes()
} }).then(function(opts) {
return Discourse.routeTo(opts.post.get('url')); opts = opts || {};
}, function(error) { _this.close();
composer.set('disableDrafts', false); if (composer.get('creatingTopic')) {
return bootbox.alert(error); Discourse.set('currentUser.topic_count', Discourse.get('currentUser.topic_count') + 1);
});
},
checkReplyLength: function() {
if (this.present('content.reply')) {
return this.set('hasReply', true);
} else { } else {
return this.set('hasReply', false); Discourse.set('currentUser.reply_count', Discourse.get('currentUser.reply_count') + 1);
} }
}, Discourse.routeTo(opts.post.get('url'));
saveDraft: function() { }, function(error) {
var model; composer.set('disableDrafts', false);
model = this.get('content'); bootbox.alert(error);
if (model) { });
return model.saveDraft(); },
}
},
/*
Open the reply view
opts: checkReplyLength: function() {
action - The action we're performing: edit, reply or createTopic if (this.present('content.reply')) {
post - The post we're replying to, if present this.set('hasReply', true);
topic - The topic we're replying to, if present } else {
quote - If we're opening a reply from a quote, the quote we're making
*/
open: function(opts) {
var composer, promise, view,
_this = this;
if (!opts) opts = {};
opts.promise = promise = opts.promise || new RSVP.Promise();
this.set('hasReply', false); this.set('hasReply', false);
if (!opts.draftKey) { }
alert("composer was opened without a draft key"); },
throw "composer opened without a proper draft key";
}
/* ensure we have a view now, without it transitions are going to be messed
*/
view = this.get('view'); saveDraft: function() {
if (!view) { var model;
view = Discourse.ComposerView.create({ model = this.get('content');
controller: this if (model) model.saveDraft();
}); },
view.appendTo(jQuery('#main'));
this.set('view', view);
/* the next runloop is too soon, need to get the control rendered and then
*/
/* we need to change stuff, otherwise css animations don't kick in /**
*/ Open the composer view
Em.run.next(function() { @method open
return Em.run.next(function() { @param {Object} opts Options for creating a post
return _this.open(opts); @param {String} opts.action The action we're performing: edit, reply or createTopic
}); @param {Discourse.Post} [opts.post] The post we're replying to
}); @param {Discourse.Topic} [opts.topic] The topic we're replying to
return promise; @param {String} [opts.quote] If we're opening a reply from a quote, the quote we're making
} **/
composer = this.get('content'); open: function(opts) {
if (composer && opts.draftKey !== composer.draftKey && composer.composeState === Discourse.Composer.DRAFT) { var composer, promise, view,
this.close(); _this = this;
composer = null; if (!opts) opts = {};
}
if (composer && !opts.tested && composer.wouldLoseChanges()) {
if (composer.composeState === Discourse.Composer.DRAFT && composer.draftKey === opts.draftKey && composer.action === opts.action) {
composer.set('composeState', Discourse.Composer.OPEN);
promise.resolve();
return promise;
} else {
opts.tested = true;
if (!opts.ignoreIfChanged) {
this.cancel((function() {
return _this.open(opts);
}), (function() {
return promise.reject();
}));
}
return promise;
}
}
/* we need a draft sequence, without it drafts are bust
*/
if (opts.draftSequence === void 0) { opts.promise = promise = opts.promise || new RSVP.Promise();
Discourse.Draft.get(opts.draftKey).then(function(data) { this.set('hasReply', false);
opts.draftSequence = data.draft_sequence; if (!opts.draftKey) {
opts.draft = data.draft; alert("composer was opened without a draft key");
throw "composer opened without a proper draft key";
}
// ensure we have a view now, without it transitions are going to be messed
view = this.get('view');
if (!view) {
view = Discourse.ComposerView.create({
controller: this
});
view.appendTo($('#main'));
this.set('view', view);
// the next runloop is too soon, need to get the control rendered and then
// we need to change stuff, otherwise css animations don't kick in
Em.run.next(function() {
return Em.run.next(function() {
return _this.open(opts); return _this.open(opts);
}); });
});
return promise;
}
composer = this.get('content');
if (composer && opts.draftKey !== composer.draftKey && composer.composeState === Discourse.Composer.DRAFT) {
this.close();
composer = null;
}
if (composer && !opts.tested && composer.wouldLoseChanges()) {
if (composer.composeState === Discourse.Composer.DRAFT && composer.draftKey === opts.draftKey && composer.action === opts.action) {
composer.set('composeState', Discourse.Composer.OPEN);
promise.resolve();
return promise;
} else {
opts.tested = true;
if (!opts.ignoreIfChanged) {
this.cancel((function() {
return _this.open(opts);
}), (function() {
return promise.reject();
}));
}
return promise; return promise;
} }
if (opts.draft) {
composer = Discourse.Composer.loadDraft(opts.draftKey, opts.draftSequence, opts.draft);
if (composer) {
composer.set('topic', opts.topic);
}
}
composer = composer || Discourse.Composer.open(opts);
this.set('content', composer);
this.set('view.content', composer);
promise.resolve();
return promise;
},
wouldLoseChanges: function() {
var composer;
composer = this.get('content');
return composer && composer.wouldLoseChanges();
},
/* View a new reply we've made
*/
viewNewReply: function() {
Discourse.routeTo(this.get('createdPost.url'));
this.close();
return false;
},
destroyDraft: function() {
var key;
key = this.get('content.draftKey');
if (key) {
return Discourse.Draft.clear(key, this.get('content.draftSequence'));
}
},
cancel: function(success, fail) {
var _this = this;
if (this.get('content.hasMetaData') || ((this.get('content.reply') || "") !== (this.get('content.originalText') || ""))) {
bootbox.confirm(Em.String.i18n("post.abandon"), Em.String.i18n("no_value"), Em.String.i18n("yes_value"), function(result) {
if (result) {
_this.destroyDraft();
_this.close();
if (typeof success === "function") {
return success();
}
} else {
if (typeof fail === "function") {
return fail();
}
}
});
} else {
/* it is possible there is some sort of crazy draft with no body ... just give up on it
*/
this.destroyDraft();
this.close();
if (typeof success === "function") {
success();
}
}
},
click: function() {
if (this.get('content.composeState') === Discourse.Composer.DRAFT) {
return this.set('content.composeState', Discourse.Composer.OPEN);
}
},
shrink: function() {
if (this.get('content.reply') === this.get('content.originalText')) {
return this.close();
} else {
return this.collapse();
}
},
collapse: function() {
this.saveDraft();
return this.set('content.composeState', Discourse.Composer.DRAFT);
},
close: function() {
this.set('content', null);
return this.set('view.content', null);
},
closeIfCollapsed: function() {
if (this.get('content.composeState') === Discourse.Composer.DRAFT) {
return this.close();
}
},
closeAutocomplete: function() {
return jQuery('#wmd-input').autocomplete({
cancel: true
});
},
/* Toggle the reply view
*/
toggle: function() {
this.closeAutocomplete();
switch (this.get('content.composeState')) {
case Discourse.Composer.OPEN:
if (this.blank('content.reply') && this.blank('content.title')) {
this.close();
} else {
this.shrink();
}
break;
case Discourse.Composer.DRAFT:
this.set('content.composeState', Discourse.Composer.OPEN);
break;
case Discourse.Composer.SAVING:
this.close();
}
return false;
},
/* ESC key hit
*/
hitEsc: function() {
if (this.get('content.composeState') === Discourse.Composer.OPEN) {
return this.shrink();
}
},
showOptions: function() {
var _ref;
return (_ref = this.get('controllers.modal')) ? _ref.show(Discourse.ArchetypeOptionsModalView.create({
archetype: this.get('content.archetype'),
metaData: this.get('content.metaData')
})) : void 0;
} }
});
}).call(this); // we need a draft sequence, without it drafts are bust
if (opts.draftSequence === void 0) {
Discourse.Draft.get(opts.draftKey).then(function(data) {
opts.draftSequence = data.draft_sequence;
opts.draft = data.draft;
return _this.open(opts);
});
return promise;
}
if (opts.draft) {
composer = Discourse.Composer.loadDraft(opts.draftKey, opts.draftSequence, opts.draft);
if (composer) {
composer.set('topic', opts.topic);
}
}
composer = composer || Discourse.Composer.open(opts);
this.set('content', composer);
this.set('view.content', composer);
promise.resolve();
return promise;
},
wouldLoseChanges: function() {
var composer;
composer = this.get('content');
return composer && composer.wouldLoseChanges();
},
// View a new reply we've made
viewNewReply: function() {
Discourse.routeTo(this.get('createdPost.url'));
this.close();
return false;
},
destroyDraft: function() {
var key;
key = this.get('content.draftKey');
if (key) {
return Discourse.Draft.clear(key, this.get('content.draftSequence'));
}
},
cancel: function(success, fail) {
var _this = this;
if (this.get('content.hasMetaData') || ((this.get('content.reply') || "") !== (this.get('content.originalText') || ""))) {
bootbox.confirm(Em.String.i18n("post.abandon"), Em.String.i18n("no_value"), Em.String.i18n("yes_value"), function(result) {
if (result) {
_this.destroyDraft();
_this.close();
if (typeof success === "function") {
return success();
}
} else {
if (typeof fail === "function") {
return fail();
}
}
});
} else {
// it is possible there is some sort of crazy draft with no body ... just give up on it
this.destroyDraft();
this.close();
if (typeof success === "function") {
success();
}
}
},
click: function() {
if (this.get('content.composeState') === Discourse.Composer.DRAFT) {
return this.set('content.composeState', Discourse.Composer.OPEN);
}
},
shrink: function() {
if (this.get('content.reply') === this.get('content.originalText')) {
return this.close();
} else {
return this.collapse();
}
},
collapse: function() {
this.saveDraft();
this.set('content.composeState', Discourse.Composer.DRAFT);
},
close: function() {
this.set('content', null);
this.set('view.content', null);
},
closeIfCollapsed: function() {
if (this.get('content.composeState') === Discourse.Composer.DRAFT) {
this.close();
}
},
closeAutocomplete: function() {
$('#wmd-input').autocomplete({ cancel: true });
},
// Toggle the reply view
toggle: function() {
this.closeAutocomplete();
switch (this.get('content.composeState')) {
case Discourse.Composer.OPEN:
if (this.blank('content.reply') && this.blank('content.title')) {
this.close();
} else {
this.shrink();
}
break;
case Discourse.Composer.DRAFT:
this.set('content.composeState', Discourse.Composer.OPEN);
break;
case Discourse.Composer.SAVING:
this.close();
}
return false;
},
// ESC key hit
hitEsc: function() {
if (this.get('content.composeState') === Discourse.Composer.OPEN) {
this.shrink();
}
},
showOptions: function() {
var _ref;
return (_ref = this.get('controllers.modal')) ? _ref.show(Discourse.ArchetypeOptionsModalView.create({
archetype: this.get('content.archetype'),
metaData: this.get('content.metaData')
})) : void 0;
}
});

View File

@ -1,5 +1,12 @@
(function() { /**
A base controller for Discourse that includes Presence support.
@class Controller
@extends Ember.Controller
@namespace Discourse
@uses Discourse.Presence
@module Discourse
**/
Discourse.Controller = Ember.Controller.extend(Discourse.Presence);
Discourse.Controller = Ember.Controller.extend(Discourse.Presence);
}).call(this);

View File

@ -1,15 +1,21 @@
(function() { /**
This controller supports actions on the site header
@class HeaderController
@extends Discourse.Controller
@namespace Discourse
@module Discourse
**/
Discourse.HeaderController = Discourse.Controller.extend({
topic: null,
showExtraInfo: false,
toggleStar: function() {
var topic = this.get('topic');
if (topic) topic.toggleStar();
return false;
}
});
Discourse.HeaderController = Ember.Controller.extend(Discourse.Presence, {
topic: null,
showExtraInfo: false,
toggleStar: function() {
var _ref;
if (_ref = this.get('topic')) {
_ref.toggleStar();
}
return false;
}
});
}).call(this);

View File

@ -1,34 +1,43 @@
(function() { /**
This controller supports actions when listing categories
@class ListCategoriesController
@extends Discourse.ObjectController
@namespace Discourse
@module Discourse
**/
Discourse.ListCategoriesController = Discourse.ObjectController.extend({
needs: ['modal'],
categoriesEven: (function() {
if (this.blank('categories')) {
return Em.A();
}
return this.get('categories').filter(function(item, index) {
return (index % 2) === 0;
});
}).property('categories.@each'),
categoriesOdd: (function() {
if (this.blank('categories')) {
return Em.A();
}
return this.get('categories').filter(function(item, index) {
return (index % 2) === 1;
});
}).property('categories.@each'),
editCategory: function(category) {
this.get('controllers.modal').show(Discourse.EditCategoryView.create({ category: category }));
return false;
},
canEdit: (function() {
var u;
u = Discourse.get('currentUser');
return u && u.admin;
}).property()
});
Discourse.ListCategoriesController = Ember.ObjectController.extend(Discourse.Presence, {
needs: ['modal'],
categoriesEven: (function() {
if (this.blank('categories')) {
return Em.A();
}
return this.get('categories').filter(function(item, index) {
return (index % 2) === 0;
});
}).property('categories.@each'),
categoriesOdd: (function() {
if (this.blank('categories')) {
return Em.A();
}
return this.get('categories').filter(function(item, index) {
return (index % 2) === 1;
});
}).property('categories.@each'),
editCategory: function(category) {
this.get('controllers.modal').show(Discourse.EditCategoryView.create({
category: category
}));
return false;
},
canEdit: (function() {
var u;
u = Discourse.get('currentUser');
return u && u.admin;
}).property()
});
}).call(this);

View File

@ -1,97 +1,105 @@
(function() { /**
This controller supports actions when listing topics or categories
Discourse.ListController = Ember.Controller.extend(Discourse.Presence, { @class ListController
currentUserBinding: 'Discourse.currentUser', @extends Discourse.Controller
categoriesBinding: 'Discourse.site.categories', @namespace Discourse
categoryBinding: 'topicList.category', @module Discourse
canCreateCategory: false, **/
canCreateTopic: false, Discourse.ListController = Discourse.Controller.extend({
needs: ['composer', 'modal', 'listTopics'], currentUserBinding: 'Discourse.currentUser',
availableNavItems: (function() { categoriesBinding: 'Discourse.site.categories',
var hasCategories, loggedOn, summary; categoryBinding: 'topicList.category',
summary = this.get('filterSummary'); canCreateCategory: false,
loggedOn = !!Discourse.get('currentUser'); canCreateTopic: false,
hasCategories = !!this.get('categories'); needs: ['composer', 'modal', 'listTopics'],
return Discourse.SiteSettings.top_menu.split("|").map(function(i) {
return Discourse.NavItem.fromText(i, { availableNavItems: (function() {
loggedOn: loggedOn, var hasCategories, loggedOn, summary;
hasCategories: hasCategories, summary = this.get('filterSummary');
countSummary: summary loggedOn = !!Discourse.get('currentUser');
}); hasCategories = !!this.get('categories');
}).filter(function(i) { return Discourse.SiteSettings.top_menu.split("|").map(function(i) {
return i !== null; return Discourse.NavItem.fromText(i, {
loggedOn: loggedOn,
hasCategories: hasCategories,
countSummary: summary
}); });
}).property('filterSummary'), }).filter(function(i) {
load: function(filterMode) { return i !== null;
var current, });
_this = this; }).property('filterSummary'),
this.set('loading', true);
if (filterMode === 'categories') {
return Ember.Deferred.promise(function(deferred) {
return Discourse.CategoryList.list(filterMode).then(function(items) {
_this.set('loading', false);
_this.set('filterMode', filterMode);
_this.set('categoryMode', true);
return deferred.resolve(items);
});
});
} else {
current = (this.get('availableNavItems').filter(function(f) {
return f.name === filterMode;
}))[0];
if (!current) {
current = Discourse.NavItem.create({
name: filterMode
});
}
return Ember.Deferred.promise(function(deferred) {
return Discourse.TopicList.list(current).then(function(items) {
_this.set('filterSummary', items.filter_summary);
_this.set('filterMode', filterMode);
_this.set('loading', false);
return deferred.resolve(items);
});
});
}
},
/* Put in the appropriate page title based on our view
*/
updateTitle: (function() { load: function(filterMode) {
if (this.get('filterMode') === 'categories') { var current,
return Discourse.set('title', Em.String.i18n('categories_list')); _this = this;
} else { this.set('loading', true);
if (this.present('category')) { if (filterMode === 'categories') {
return Discourse.set('title', "" + (this.get('category.name').capitalize()) + " " + (Em.String.i18n('topic.list'))); return Ember.Deferred.promise(function(deferred) {
} else { return Discourse.CategoryList.list(filterMode).then(function(items) {
return Discourse.set('title', Em.String.i18n('topic.list')); _this.set('loading', false);
} _this.set('filterMode', filterMode);
} _this.set('categoryMode', true);
}).observes('filterMode', 'category'), return deferred.resolve(items);
/* Create topic button });
*/ });
} else {
createTopic: function() { current = (this.get('availableNavItems').filter(function(f) {
var topicList; return f.name === filterMode;
topicList = this.get('controllers.listTopics.content'); }))[0];
if (!topicList) { if (!current) {
return; current = Discourse.NavItem.create({
} name: filterMode
return this.get('controllers.composer').open({ });
categoryName: this.get('category.name'), }
action: Discourse.Composer.CREATE_TOPIC, return Ember.Deferred.promise(function(deferred) {
draftKey: topicList.get('draft_key'), return Discourse.TopicList.list(current).then(function(items) {
draftSequence: topicList.get('draft_sequence') _this.set('filterSummary', items.filter_summary);
_this.set('filterMode', filterMode);
_this.set('loading', false);
return deferred.resolve(items);
});
}); });
},
createCategory: function() {
var _ref;
return (_ref = this.get('controllers.modal')) ? _ref.show(Discourse.EditCategoryView.create()) : void 0;
} }
}); },
// Put in the appropriate page title based on our view
updateTitle: (function() {
if (this.get('filterMode') === 'categories') {
return Discourse.set('title', Em.String.i18n('categories_list'));
} else {
if (this.present('category')) {
return Discourse.set('title', "" + (this.get('category.name').capitalize()) + " " + (Em.String.i18n('topic.list')));
} else {
return Discourse.set('title', Em.String.i18n('topic.list'));
}
}
}).observes('filterMode', 'category'),
// Create topic button
createTopic: function() {
var topicList;
topicList = this.get('controllers.listTopics.content');
if (!topicList) {
return;
}
return this.get('controllers.composer').open({
categoryName: this.get('category.name'),
action: Discourse.Composer.CREATE_TOPIC,
draftKey: topicList.get('draft_key'),
draftSequence: topicList.get('draft_sequence')
});
},
createCategory: function() {
var _ref;
return (_ref = this.get('controllers.modal')) ? _ref.show(Discourse.EditCategoryView.create()) : void 0;
}
});
Discourse.ListController.reopenClass({
filters: ['popular', 'favorited', 'read', 'unread', 'new', 'posted']
});
Discourse.ListController.reopenClass({
filters: ['popular', 'favorited', 'read', 'unread', 'new', 'posted']
});
}).call(this);

View File

@ -1,73 +1,73 @@
(function() { /**
This controller supports actions when listing topics or categories
Discourse.ListTopicsController = Ember.ObjectController.extend({ @class ListTopicsController
needs: ['list', 'composer'], @extends Discourse.ObjectController
/* If we're changing our channel @namespace Discourse
*/ @module Discourse
**/
Discourse.ListTopicsController = Discourse.ObjectController.extend({
needs: ['list', 'composer'],
// If we're changing our channel
previousChannel: null,
previousChannel: null, popular: (function() {
popular: (function() { return this.get('content.filter') === 'popular';
return this.get('content.filter') === 'popular'; }).property('content.filter'),
}).property('content.filter'),
filterModeChanged: (function() {
/* Unsubscribe from a previous channel if necessary
*/
var channel, filterMode, previousChannel, filterModeChanged: (function() {
_this = this; // Unsubscribe from a previous channel if necessary
if (previousChannel = this.get('previousChannel')) { var channel, filterMode, previousChannel,
Discourse.MessageBus.unsubscribe("/" + previousChannel); _this = this;
this.set('previousChannel', null); if (previousChannel = this.get('previousChannel')) {
} Discourse.MessageBus.unsubscribe("/" + previousChannel);
filterMode = this.get('controllers.list.filterMode'); this.set('previousChannel', null);
if (!filterMode) {
return;
}
channel = filterMode;
Discourse.MessageBus.subscribe("/" + channel, function(data) {
return _this.get('content').insert(data);
});
return this.set('previousChannel', channel);
}).observes('controllers.list.filterMode'),
draftLoaded: (function() {
var draft;
draft = this.get('content.draft');
if (draft) {
return this.get('controllers.composer').open({
draft: draft,
draftKey: this.get('content.draft_key'),
draftSequence: this.get('content.draft_sequence'),
ignoreIfChanged: true
});
}
}).observes('content.draft'),
/* Star a topic
*/
toggleStar: function(topic) {
topic.toggleStar();
return false;
},
createTopic: function() {
this.get('controllers.list').createTopic();
return false;
},
observer: (function() {
return this.set('filterMode', this.get('controllser.list.filterMode'));
}).observes('controller.list.filterMode'),
/* Show newly inserted topics
*/
showInserted: function(e) {
/* Move inserted into topics
*/
this.get('content.topics').unshiftObjects(this.get('content.inserted'));
/* Clear inserted
*/
this.set('content.inserted', Em.A());
return false;
} }
}); filterMode = this.get('controllers.list.filterMode');
if (!filterMode) {
return;
}
channel = filterMode;
Discourse.MessageBus.subscribe("/" + channel, function(data) {
return _this.get('content').insert(data);
});
return this.set('previousChannel', channel);
}).observes('controllers.list.filterMode'),
draftLoaded: (function() {
var draft;
draft = this.get('content.draft');
if (draft) {
return this.get('controllers.composer').open({
draft: draft,
draftKey: this.get('content.draft_key'),
draftSequence: this.get('content.draft_sequence'),
ignoreIfChanged: true
});
}
}).observes('content.draft'),
// Star a topic
toggleStar: function(topic) {
topic.toggleStar();
},
createTopic: function() {
this.get('controllers.list').createTopic();
},
observer: (function() {
return this.set('filterMode', this.get('controllser.list.filterMode'));
}).observes('controller.list.filterMode'),
// Show newly inserted topics
showInserted: function(e) {
// Move inserted into topics
this.get('content.topics').unshiftObjects(this.get('content.inserted'));
// Clear inserted
this.set('content.inserted', Em.A());
return false;
}
});
}).call(this);

View File

@ -1,9 +1,15 @@
(function() { /**
This controller supports actions related to showing modals
@class ModalController
@extends Discourse.Controller
@namespace Discourse
@module Discourse
**/
Discourse.ModalController = Discourse.Controller.extend({
show: function(view) {
this.set('currentView', view);
}
});
Discourse.ModalController = Ember.Controller.extend(Discourse.Presence, {
show: function(view) {
return this.set('currentView', view);
}
});
}).call(this);

View File

@ -0,0 +1,12 @@
/**
A custom object controller for Discourse
@class ObjectController
@extends Ember.ObjectController
@namespace Discourse
@uses Discourse.Presence
@module Discourse
**/
Discourse.ObjectController = Ember.ObjectController.extend(Discourse.Presence);

View File

@ -1,158 +1,89 @@
(function() { /**
This controller supports actions related to updating one's preferences
Discourse.PreferencesController = Ember.ObjectController.extend(Discourse.Presence, { @class PreferencesController
/* By default we haven't saved anything @extends Discourse.ObjectController
*/ @namespace Discourse
@module Discourse
**/
Discourse.PreferencesController = Discourse.ObjectController.extend({
// By default we haven't saved anything
saved: false,
saved: false, saveDisabled: (function() {
saveDisabled: (function() { if (this.get('saving')) return true;
if (this.get('saving')) { if (this.blank('content.name')) return true;
return true; if (this.blank('content.email')) return true;
} return false;
if (this.blank('content.name')) { }).property('saving', 'content.name', 'content.email'),
return true;
}
if (this.blank('content.email')) {
return true;
}
return false;
}).property('saving', 'content.name', 'content.email'),
digestFrequencies: (function() {
var freqs;
freqs = Em.A();
freqs.addObject({
name: Em.String.i18n('user.email_digests.daily'),
value: 1
});
freqs.addObject({
name: Em.String.i18n('user.email_digests.weekly'),
value: 7
});
freqs.addObject({
name: Em.String.i18n('user.email_digests.bi_weekly'),
value: 14
});
return freqs;
}).property(),
autoTrackDurations: (function() {
var freqs;
freqs = Em.A();
freqs.addObject({
name: Em.String.i18n('user.auto_track_options.never'),
value: -1
});
freqs.addObject({
name: Em.String.i18n('user.auto_track_options.always'),
value: 0
});
freqs.addObject({
name: Em.String.i18n('user.auto_track_options.after_n_seconds', {
count: 30
}),
value: 30000
});
freqs.addObject({
name: Em.String.i18n('user.auto_track_options.after_n_minutes', {
count: 1
}),
value: 60000
});
freqs.addObject({
name: Em.String.i18n('user.auto_track_options.after_n_minutes', {
count: 2
}),
value: 120000
});
freqs.addObject({
name: Em.String.i18n('user.auto_track_options.after_n_minutes', {
count: 3
}),
value: 180000
});
freqs.addObject({
name: Em.String.i18n('user.auto_track_options.after_n_minutes', {
count: 4
}),
value: 240000
});
freqs.addObject({
name: Em.String.i18n('user.auto_track_options.after_n_minutes', {
count: 5
}),
value: 300000
});
freqs.addObject({
name: Em.String.i18n('user.auto_track_options.after_n_minutes', {
count: 10
}),
value: 600000
});
return freqs;
}).property(),
considerNewTopicOptions: (function() {
var opts;
opts = Em.A();
opts.addObject({
name: Em.String.i18n('user.new_topic_duration.not_viewed'),
value: -1
});
opts.addObject({
name: Em.String.i18n('user.new_topic_duration.after_n_days', {
count: 1
}),
value: 60 * 24
});
opts.addObject({
name: Em.String.i18n('user.new_topic_duration.after_n_days', {
count: 2
}),
value: 60 * 48
});
opts.addObject({
name: Em.String.i18n('user.new_topic_duration.after_n_weeks', {
count: 1
}),
value: 7 * 60 * 24
});
opts.addObject({
name: Em.String.i18n('user.new_topic_duration.last_here'),
value: -2
});
return opts;
}).property(),
save: function() {
var _this = this;
this.set('saving', true);
this.set('saved', false);
/* Cook the bio for preview
*/
return this.get('content').save(function(result) { digestFrequencies: (function() {
_this.set('saving', false); var freqs;
if (result) { freqs = Em.A();
_this.set('content.bio_cooked', Discourse.Utilities.cook(_this.get('content.bio_raw'))); freqs.addObject({ name: Em.String.i18n('user.email_digests.daily'), value: 1 });
return _this.set('saved', true); freqs.addObject({ name: Em.String.i18n('user.email_digests.weekly'), value: 7 });
} else { freqs.addObject({ name: Em.String.i18n('user.email_digests.bi_weekly'), value: 14 });
return alert('failed'); return freqs;
} }).property(),
autoTrackDurations: (function() {
var freqs;
freqs = Em.A();
freqs.addObject({ name: Em.String.i18n('user.auto_track_options.never'), value: -1 });
freqs.addObject({ name: Em.String.i18n('user.auto_track_options.always'), value: 0 });
freqs.addObject({ name: Em.String.i18n('user.auto_track_options.after_n_seconds', { count: 30 }), value: 30000 });
freqs.addObject({ name: Em.String.i18n('user.auto_track_options.after_n_minutes', { count: 1 }), value: 60000 });
freqs.addObject({ name: Em.String.i18n('user.auto_track_options.after_n_minutes', { count: 2 }), value: 120000 });
freqs.addObject({ name: Em.String.i18n('user.auto_track_options.after_n_minutes', { count: 3 }), value: 180000 });
freqs.addObject({ name: Em.String.i18n('user.auto_track_options.after_n_minutes', { count: 4 }), value: 240000 });
freqs.addObject({ name: Em.String.i18n('user.auto_track_options.after_n_minutes', { count: 5 }), value: 300000 });
freqs.addObject({ name: Em.String.i18n('user.auto_track_options.after_n_minutes', { count: 10 }), value: 600000 });
return freqs;
}).property(),
considerNewTopicOptions: (function() {
var opts;
opts = Em.A();
opts.addObject({ name: Em.String.i18n('user.new_topic_duration.not_viewed'), value: -1 });
opts.addObject({ name: Em.String.i18n('user.new_topic_duration.after_n_days', { count: 1 }), value: 60 * 24 });
opts.addObject({ name: Em.String.i18n('user.new_topic_duration.after_n_days', { count: 2 }), value: 60 * 48 });
opts.addObject({ name: Em.String.i18n('user.new_topic_duration.after_n_weeks', { count: 1 }), value: 7 * 60 * 24 });
opts.addObject({ name: Em.String.i18n('user.new_topic_duration.last_here'), value: -2 });
return opts;
}).property(),
save: function() {
var _this = this;
this.set('saving', true);
this.set('saved', false);
// Cook the bio for preview
return this.get('content').save(function(result) {
_this.set('saving', false);
if (result) {
_this.set('content.bio_cooked', Discourse.Utilities.cook(_this.get('content.bio_raw')));
return _this.set('saved', true);
} else {
return alert('failed');
}
});
},
saveButtonText: (function() {
if (this.get('saving')) return Em.String.i18n('saving');
return Em.String.i18n('save');
}).property('saving'),
changePassword: function() {
var _this = this;
if (!this.get('passwordProgress')) {
this.set('passwordProgress', '(generating email)');
return this.get('content').changePassword(function(message) {
_this.set('changePasswordProgress', false);
return _this.set('passwordProgress', "(" + message + ")");
}); });
},
saveButtonText: (function() {
if (this.get('saving')) {
return Em.String.i18n('saving');
}
return Em.String.i18n('save');
}).property('saving'),
changePassword: function() {
var _this = this;
if (!this.get('passwordProgress')) {
this.set('passwordProgress', '(generating email)');
return this.get('content').changePassword(function(message) {
_this.set('changePasswordProgress', false);
return _this.set('passwordProgress', "(" + message + ")");
});
}
} }
}); }
});
}).call(this);

View File

@ -1,48 +1,50 @@
(function() { /**
This controller supports actions related to updating one's email address
@class PreferencesEmailController
@extends Discourse.ObjectController
@namespace Discourse
@module Discourse
**/
Discourse.PreferencesEmailController = Discourse.ObjectController.extend({
taken: false,
saving: false,
error: false,
success: false,
saveDisabled: (function() {
if (this.get('saving')) return true;
if (this.blank('newEmail')) return true;
if (this.get('taken')) return true;
if (this.get('unchanged')) return true;
}).property('newEmail', 'taken', 'unchanged', 'saving'),
unchanged: (function() {
return this.get('newEmail') === this.get('content.email');
}).property('newEmail', 'content.email'),
initializeEmail: (function() {
this.set('newEmail', this.get('content.email'));
}).observes('content.email'),
saveButtonText: (function() {
if (this.get('saving')) return Em.String.i18n("saving");
return Em.String.i18n("user.change_email.action");
}).property('saving'),
changeEmail: function() {
var _this = this;
this.set('saving', true);
return this.get('content').changeEmail(this.get('newEmail')).then(function() {
return _this.set('success', true);
}, function() {
/* Error
*/
_this.set('error', true);
return _this.set('saving', false);
});
}
});
Discourse.PreferencesEmailController = Ember.ObjectController.extend(Discourse.Presence, {
taken: false,
saving: false,
error: false,
success: false,
saveDisabled: (function() {
if (this.get('saving')) {
return true;
}
if (this.blank('newEmail')) {
return true;
}
if (this.get('taken')) {
return true;
}
if (this.get('unchanged')) {
return true;
}
}).property('newEmail', 'taken', 'unchanged', 'saving'),
unchanged: (function() {
return this.get('newEmail') === this.get('content.email');
}).property('newEmail', 'content.email'),
initializeEmail: (function() {
return this.set('newEmail', this.get('content.email'));
}).observes('content.email'),
saveButtonText: (function() {
if (this.get('saving')) {
return Em.String.i18n("saving");
}
return Em.String.i18n("user.change_email.action");
}).property('saving'),
changeEmail: function() {
var _this = this;
this.set('saving', true);
return this.get('content').changeEmail(this.get('newEmail')).then(function() {
return _this.set('success', true);
}, function() {
/* Error
*/
_this.set('error', true);
return _this.set('saving', false);
});
}
});
}).call(this);

View File

@ -1,71 +1,66 @@
(function() { /**
This controller supports actions related to updating one's username
@class PreferencesUsernameController
@extends Discourse.ObjectController
@namespace Discourse
@module Discourse
**/
Discourse.PreferencesUsernameController = Discourse.ObjectController.extend({
taken: false,
saving: false,
error: false,
errorMessage: null,
saveDisabled: (function() {
if (this.get('saving')) return true;
if (this.blank('newUsername')) return true;
if (this.get('taken')) return true;
if (this.get('unchanged')) return true;
if (this.get('errorMessage')) return true;
return false;
}).property('newUsername', 'taken', 'errorMessage', 'unchanged', 'saving'),
unchanged: (function() {
return this.get('newUsername') === this.get('content.username');
}).property('newUsername', 'content.username'),
checkTaken: (function() {
var _this = this;
this.set('taken', false);
this.set('errorMessage', null);
if (this.blank('newUsername')) return;
if (this.get('unchanged')) return;
Discourse.User.checkUsername(this.get('newUsername')).then(function(result) {
if (result.errors) {
return _this.set('errorMessage', result.errors.join(' '));
} else if (result.available === false) {
return _this.set('taken', true);
}
});
}).observes('newUsername'),
saveButtonText: (function() {
if (this.get('saving')) return Em.String.i18n("saving");
return Em.String.i18n("user.change_username.action");
}).property('saving'),
changeUsername: function() {
var _this = this;
return bootbox.confirm(Em.String.i18n("user.change_username.confirm"), Em.String.i18n("no_value"), Em.String.i18n("yes_value"), function(result) {
if (result) {
_this.set('saving', true);
return _this.get('content').changeUsername(_this.get('newUsername')).then(function() {
window.location = "/users/" + (_this.get('newUsername').toLowerCase()) + "/preferences";
}, function() {
/* Error
*/
_this.set('error', true);
return _this.set('saving', false);
});
}
});
}
});
Discourse.PreferencesUsernameController = Ember.ObjectController.extend(Discourse.Presence, {
taken: false,
saving: false,
error: false,
errorMessage: null,
saveDisabled: (function() {
if (this.get('saving')) {
return true;
}
if (this.blank('newUsername')) {
return true;
}
if (this.get('taken')) {
return true;
}
if (this.get('unchanged')) {
return true;
}
if (this.get('errorMessage')) {
return true;
}
return false;
}).property('newUsername', 'taken', 'errorMessage', 'unchanged', 'saving'),
unchanged: (function() {
return this.get('newUsername') === this.get('content.username');
}).property('newUsername', 'content.username'),
checkTaken: (function() {
var _this = this;
this.set('taken', false);
this.set('errorMessage', null);
if (this.blank('newUsername')) {
return;
}
if (this.get('unchanged')) {
return;
}
return Discourse.User.checkUsername(this.get('newUsername')).then(function(result) {
if (result.errors) {
return _this.set('errorMessage', result.errors.join(' '));
} else if (result.available === false) {
return _this.set('taken', true);
}
});
}).observes('newUsername'),
saveButtonText: (function() {
if (this.get('saving')) {
return Em.String.i18n("saving");
}
return Em.String.i18n("user.change_username.action");
}).property('saving'),
changeUsername: function() {
var _this = this;
return bootbox.confirm(Em.String.i18n("user.change_username.confirm"), Em.String.i18n("no_value"), Em.String.i18n("yes_value"), function(result) {
if (result) {
_this.set('saving', true);
return _this.get('content').changeUsername(_this.get('newUsername')).then(function() {
window.location = "/users/" + (_this.get('newUsername').toLowerCase()) + "/preferences";
}, function() {
/* Error
*/
_this.set('error', true);
return _this.set('saving', false);
});
}
});
}
});
}).call(this);

View File

@ -1,86 +1,86 @@
(function() { /**
This controller supports the pop up quote button
Discourse.QuoteButtonController = Discourse.Controller.extend({ @class QuoteButtonController
needs: ['topic', 'composer'], @extends Discourse.Controller
started: null, @namespace Discourse
/* If the buffer is cleared, clear out other state (post) @module Discourse
*/ **/
Discourse.QuoteButtonController = Discourse.Controller.extend({
needs: ['topic', 'composer'],
started: null,
bufferChanged: (function() { // If the buffer is cleared, clear out other state (post)
if (this.blank('buffer')) { bufferChanged: (function() {
return this.set('post', null); if (this.blank('buffer')) {
} return this.set('post', null);
}).observes('buffer'),
mouseDown: function(e) {
this.started = [e.pageX, e.pageY];
},
mouseUp: function(e) {
if (this.started[1] > e.pageY) {
this.started = [e.pageX, e.pageY];
}
},
selectText: function(e) {
var $quoteButton, left, selectedText, top;
if (!Discourse.get('currentUser')) {
return;
}
if (!this.get('controllers.topic.content.can_create_post')) {
return;
}
selectedText = Discourse.Utilities.selectedText();
if (this.get('buffer') === selectedText) {
return;
}
if (this.get('lastSelected') === selectedText) {
return;
}
this.set('post', e.context);
this.set('buffer', selectedText);
top = e.pageY + 5;
left = e.pageX + 5;
$quoteButton = jQuery('.quote-button');
if (this.started) {
top = this.started[1] - 50;
left = ((left - this.started[0]) / 2) + this.started[0] - ($quoteButton.width() / 2);
}
$quoteButton.css({
top: top,
left: left
});
this.started = null;
return false;
},
quoteText: function(e) {
var buffer, composerController, composerOpts, composerPost, post, quotedText,
_this = this;
e.stopPropagation();
post = this.get('post');
composerController = this.get('controllers.composer');
composerOpts = {
post: post,
action: Discourse.Composer.REPLY,
draftKey: this.get('post.topic.draft_key')
};
/* If the composer is associated with a different post, we don't change it.
*/
if (composerPost = composerController.get('content.post')) {
if (composerPost.get('id') !== this.get('post.id')) {
composerOpts.post = composerPost;
}
}
buffer = this.get('buffer');
quotedText = Discourse.BBCode.buildQuoteBBCode(post, buffer);
if (composerController.wouldLoseChanges()) {
composerController.appendText(quotedText);
} else {
composerController.open(composerOpts).then(function() {
return composerController.appendText(quotedText);
});
}
this.set('buffer', '');
return false;
} }
}); }).observes('buffer'),
}).call(this); mouseDown: function(e) {
this.started = [e.pageX, e.pageY];
},
mouseUp: function(e) {
if (this.started[1] > e.pageY) {
this.started = [e.pageX, e.pageY];
}
},
selectText: function(e) {
var $quoteButton, left, selectedText, top;
if (!Discourse.get('currentUser')) return;
if (!this.get('controllers.topic.content.can_create_post')) return;
selectedText = Discourse.Utilities.selectedText();
if (this.get('buffer') === selectedText) return;
if (this.get('lastSelected') === selectedText) return;
this.set('post', e.context);
this.set('buffer', selectedText);
top = e.pageY + 5;
left = e.pageX + 5;
$quoteButton = $('.quote-button');
if (this.started) {
top = this.started[1] - 50;
left = ((left - this.started[0]) / 2) + this.started[0] - ($quoteButton.width() / 2);
}
$quoteButton.css({
top: top,
left: left
});
this.started = null;
return false;
},
quoteText: function(e) {
var buffer, composerController, composerOpts, composerPost, post, quotedText,
_this = this;
e.stopPropagation();
post = this.get('post');
composerController = this.get('controllers.composer');
composerOpts = {
post: post,
action: Discourse.Composer.REPLY,
draftKey: this.get('post.topic.draft_key')
};
// If the composer is associated with a different post, we don't change it.
if (composerPost = composerController.get('content.post')) {
if (composerPost.get('id') !== this.get('post.id')) {
composerOpts.post = composerPost;
}
}
buffer = this.get('buffer');
quotedText = Discourse.BBCode.buildQuoteBBCode(post, buffer);
if (composerController.wouldLoseChanges()) {
composerController.appendText(quotedText);
} else {
composerController.open(composerOpts).then(function() {
return composerController.appendText(quotedText);
});
}
this.set('buffer', '');
return false;
}
});

View File

@ -1,29 +1,33 @@
(function() { /**
This controller supports the "share" link controls
Discourse.ShareController = Ember.Controller.extend({ @class ShareController
/* When the user clicks the post number, we pop up a share box @extends Discourse.Controller
*/ @namespace Discourse
@module Discourse
**/
Discourse.ShareController = Discourse.Controller.extend({
shareLink: function(e, url) { // When the user clicks the post number, we pop up a share box
var x; shareLink: function(e, url) {
x = e.pageX - 150; var x;
if (x < 25) { x = e.pageX - 150;
x = 25; if (x < 25) {
} x = 25;
jQuery('#share-link').css({
left: "" + x + "px",
top: "" + (e.pageY - 100) + "px"
});
this.set('link', url);
return false;
},
/* Close the share controller
*/
close: function() {
this.set('link', '');
return false;
} }
}); $('#share-link').css({
left: "" + x + "px",
top: "" + (e.pageY - 100) + "px"
});
this.set('link', url);
return false;
},
// Close the share controller
close: function() {
this.set('link', '');
return false;
}
});
}).call(this);

View File

@ -1,32 +1,38 @@
(function() { /**
This controller supports displaying static content.
Discourse.StaticController = Ember.Controller.extend({ @class StaticController
content: null, @extends Discourse.Controller
loadPath: function(path) { @namespace Discourse
var $preloaded, text, @module Discourse
_this = this; **/
this.set('content', null); Discourse.StaticController = Discourse.Controller.extend({
/* Load from <noscript> if we have it. content: null,
*/
$preloaded = jQuery("noscript[data-path=\"" + path + "\"]"); loadPath: function(path) {
if ($preloaded.length) { var $preloaded, text,
text = $preloaded.text(); _this = this;
text = text.replace(/<header[\s\S]*<\/header\>/, ''); this.set('content', null);
return this.set('content', text);
} else { // Load from <noscript> if we have it.
return jQuery.ajax({ $preloaded = $("noscript[data-path=\"" + path + "\"]");
url: "" + path + ".json", if ($preloaded.length) {
success: function(result) { text = $preloaded.text();
return _this.set('content', result); text = text.replace(/<header[\s\S]*<\/header\>/, '');
} return this.set('content', text);
}); } else {
} return jQuery.ajax({
url: "" + path + ".json",
success: function(result) {
return _this.set('content', result);
}
});
} }
}); }
});
Discourse.StaticController.reopenClass({
pages: ['faq', 'tos', 'privacy']
});
Discourse.StaticController.reopenClass({
pages: ['faq', 'tos', 'privacy']
});
}).call(this);

View File

@ -1,13 +1,20 @@
(function() { /**
This controller supports the admin menu on topics
Discourse.TopicAdminMenuController = Ember.ObjectController.extend({ @class TopicAdminMenuController
visible: false, @extends Discourse.ObjectController
show: function() { @namespace Discourse
return this.set('visible', true); @module Discourse
}, **/
hide: function() { Discourse.TopicAdminMenuController = Discourse.ObjectController.extend({
return this.set('visible', false); visible: false,
}
});
}).call(this); show: function() {
this.set('visible', true);
},
hide: function() {
this.set('visible', false);
}
});

View File

@ -1,419 +1,414 @@
(function() { /**
This controller supports all actions related to a topic
Discourse.TopicController = Ember.ObjectController.extend(Discourse.Presence, { @class TopicController
/* A list of usernames we want to filter by @extends Discourse.ObjectController
*/ @namespace Discourse
@module Discourse
**/
Discourse.TopicController = Discourse.ObjectController.extend({
userFilters: new Em.Set(),
multiSelect: false,
bestOf: false,
showExtraHeaderInfo: false,
needs: ['header', 'modal', 'composer', 'quoteButton'],
userFilters: new Em.Set(), filter: (function() {
multiSelect: false, if (this.get('bestOf') === true) return 'best_of';
bestOf: false, if (this.get('userFilters').length > 0) return 'user';
showExtraHeaderInfo: false, return null;
needs: ['header', 'modal', 'composer', 'quoteButton'], }).property('userFilters.[]', 'bestOf'),
filter: (function() {
if (this.get('bestOf') === true) { filterDesc: (function() {
return 'best_of'; var filter;
} if (!(filter = this.get('filter'))) return null;
if (this.get('userFilters').length > 0) { return Em.String.i18n("topic.filters." + filter);
return 'user'; }).property('filter'),
}
return null; selectedPosts: (function() {
}).property('userFilters.[]', 'bestOf'), var posts;
filterDesc: (function() { if (!(posts = this.get('content.posts'))) return null;
var filter; return posts.filterProperty('selected');
if (!(filter = this.get('filter'))) { }).property('content.posts.@each.selected'),
return null;
} selectedCount: (function() {
return Em.String.i18n("topic.filters." + filter); if (!this.get('selectedPosts')) return 0;
}).property('filter'), return this.get('selectedPosts').length;
selectedPosts: (function() { }).property('selectedPosts'),
var posts;
if (!(posts = this.get('content.posts'))) { canMoveSelected: (function() {
return null; if (!this.get('content.can_move_posts')) return false;
} // For now, we can move it if we can delete it since the posts need to be deleted.
return posts.filterProperty('selected'); return this.get('canDeleteSelected');
}).property('content.posts.@each.selected'), }).property('canDeleteSelected'),
selectedCount: (function() {
if (!this.get('selectedPosts')) { showExtraHeaderInfoChanged: (function() {
return 0; this.set('controllers.header.showExtraInfo', this.get('showExtraHeaderInfo'));
} }).observes('showExtraHeaderInfo'),
return this.get('selectedPosts').length;
}).property('selectedPosts'), canDeleteSelected: (function() {
canMoveSelected: (function() { var canDelete, selectedPosts;
if (!this.get('content.can_move_posts')) { selectedPosts = this.get('selectedPosts');
if (!(selectedPosts && selectedPosts.length > 0)) return false;
canDelete = true;
selectedPosts.each(function(p) {
if (!p.get('can_delete')) {
canDelete = false;
return false; return false;
} }
/* For now, we can move it if we can delete it since the posts });
*/ return canDelete;
}).property('selectedPosts'),
/* need to be deleted. multiSelectChanged: (function() {
*/ // Deselect all posts when multi select is turned off
var posts;
return this.get('canDeleteSelected'); if (!this.get('multiSelect')) {
}).property('canDeleteSelected'), if (posts = this.get('content.posts')) {
showExtraHeaderInfoChanged: (function() { return posts.forEach(function(p) {
return this.set('controllers.header.showExtraInfo', this.get('showExtraHeaderInfo')); return p.set('selected', false);
}).observes('showExtraHeaderInfo'),
canDeleteSelected: (function() {
var canDelete, selectedPosts;
selectedPosts = this.get('selectedPosts');
if (!(selectedPosts && selectedPosts.length > 0)) {
return false;
}
canDelete = true;
selectedPosts.each(function(p) {
if (!p.get('can_delete')) {
canDelete = false;
return false;
}
});
return canDelete;
}).property('selectedPosts'),
multiSelectChanged: (function() {
/* Deselect all posts when multi select is turned off
*/
var posts;
if (!this.get('multiSelect')) {
if (posts = this.get('content.posts')) {
return posts.forEach(function(p) {
return p.set('selected', false);
});
}
}
}).observes('multiSelect'),
hideProgress: (function() {
if (!this.get('content.loaded')) {
return true;
}
if (!this.get('currentPost')) {
return true;
}
if (this.get('content.highest_post_number') < 2) {
return true;
}
return this.present('filter');
}).property('filter', 'content.loaded', 'currentPost'),
selectPost: function(post) {
return post.toggleProperty('selected');
},
toggleMultiSelect: function() {
return this.toggleProperty('multiSelect');
},
moveSelected: function() {
var _ref;
return (_ref = this.get('controllers.modal')) ? _ref.show(Discourse.MoveSelectedView.create({
topic: this.get('content'),
selectedPosts: this.get('selectedPosts')
})) : void 0;
},
deleteSelected: function() {
var _this = this;
return bootbox.confirm(Em.String.i18n("post.delete.confirm", {
count: this.get('selectedCount')
}), function(result) {
if (result) {
Discourse.Post.deleteMany(_this.get('selectedPosts'));
return _this.get('content.posts').removeObjects(_this.get('selectedPosts'));
}
});
},
jumpTop: function() {
return Discourse.routeTo(this.get('content.url'));
},
jumpBottom: function() {
return Discourse.routeTo(this.get('content.lastPostUrl'));
},
cancelFilter: function() {
this.set('bestOf', false);
return this.get('userFilters').clear();
},
replyAsNewTopic: function(post) {
var composerController, postLink, postUrl, promise;
composerController = this.get('controllers.composer');
/*TODO shut down topic draft cleanly if it exists ...
*/
promise = composerController.open({
action: Discourse.Composer.CREATE_TOPIC,
draftKey: Discourse.Composer.REPLY_AS_NEW_TOPIC_KEY
});
postUrl = "" + location.protocol + "//" + location.host + (post.get('url'));
postLink = "[" + (this.get('title')) + "](" + postUrl + ")";
return promise.then(function() {
return Discourse.Post.loadQuote(post.get('id')).then(function(q) {
return composerController.appendText("" + (Em.String.i18n("post.continue_discussion", {
postLink: postLink
})) + "\n\n" + q);
}); });
}); }
}, }
/* Topic related }).observes('multiSelect'),
*/
reply: function() { hideProgress: (function() {
var composerController; if (!this.get('content.loaded')) return true;
composerController = this.get('controllers.composer'); if (!this.get('currentPost')) return true;
return composerController.open({ if (this.get('content.highest_post_number') < 2) return true;
topic: this.get('content'), return this.present('filter');
action: Discourse.Composer.REPLY, }).property('filter', 'content.loaded', 'currentPost'),
draftKey: this.get('content.draft_key'),
draftSequence: this.get('content.draft_sequence') selectPost: function(post) {
post.toggleProperty('selected');
},
toggleMultiSelect: function() {
this.toggleProperty('multiSelect');
},
moveSelected: function() {
var _ref;
return (_ref = this.get('controllers.modal')) ? _ref.show(Discourse.MoveSelectedView.create({
topic: this.get('content'),
selectedPosts: this.get('selectedPosts')
})) : void 0;
},
deleteSelected: function() {
var _this = this;
return bootbox.confirm(Em.String.i18n("post.delete.confirm", {
count: this.get('selectedCount')
}), function(result) {
if (result) {
Discourse.Post.deleteMany(_this.get('selectedPosts'));
return _this.get('content.posts').removeObjects(_this.get('selectedPosts'));
}
});
},
jumpTop: function() {
Discourse.routeTo(this.get('content.url'));
},
jumpBottom: function() {
Discourse.routeTo(this.get('content.lastPostUrl'));
},
cancelFilter: function() {
this.set('bestOf', false);
this.get('userFilters').clear();
},
replyAsNewTopic: function(post) {
var composerController, postLink, postUrl, promise;
composerController = this.get('controllers.composer');
// TODO shut down topic draft cleanly if it exists ...
promise = composerController.open({
action: Discourse.Composer.CREATE_TOPIC,
draftKey: Discourse.Composer.REPLY_AS_NEW_TOPIC_KEY
});
postUrl = "" + location.protocol + "//" + location.host + (post.get('url'));
postLink = "[" + (this.get('title')) + "](" + postUrl + ")";
return promise.then(function() {
return Discourse.Post.loadQuote(post.get('id')).then(function(q) {
return composerController.appendText("" + (Em.String.i18n("post.continue_discussion", {
postLink: postLink
})) + "\n\n" + q);
}); });
}, });
toggleParticipant: function(user) { },
var userFilters, username;
this.set('bestOf', false); // Topic related
username = Em.get(user, 'username'); reply: function() {
userFilters = this.get('userFilters'); var composerController;
if (userFilters.contains(username)) { composerController = this.get('controllers.composer');
userFilters.remove(username); return composerController.open({
} else { topic: this.get('content'),
userFilters.add(username); action: Discourse.Composer.REPLY,
} draftKey: this.get('content.draft_key'),
draftSequence: this.get('content.draft_sequence')
});
},
toggleParticipant: function(user) {
var userFilters, username;
this.set('bestOf', false);
username = Em.get(user, 'username');
userFilters = this.get('userFilters');
if (userFilters.contains(username)) {
userFilters.remove(username);
} else {
userFilters.add(username);
}
return false;
},
enableBestOf: function(e) {
this.set('bestOf', true);
this.get('userFilters').clear();
return false;
},
showBestOf: (function() {
if (this.get('bestOf') === true) {
return false; return false;
}, }
enableBestOf: function(e) { return this.get('content.has_best_of') === true;
this.set('bestOf', true); }).property('bestOf', 'content.has_best_of'),
this.get('userFilters').clear();
return false; postFilters: (function() {
}, if (this.get('bestOf') === true) {
showBestOf: (function() {
if (this.get('bestOf') === true) {
return false;
}
return this.get('content.has_best_of') === true;
}).property('bestOf', 'content.has_best_of'),
postFilters: (function() {
if (this.get('bestOf') === true) {
return {
bestOf: true
};
}
return { return {
userFilters: this.get('userFilters') bestOf: true
}; };
}).property('userFilters.[]', 'bestOf'), }
reloadTopics: (function() { return {
var posts, topic, userFilters: this.get('userFilters')
_this = this; };
topic = this.get('content'); }).property('userFilters.[]', 'bestOf'),
if (!topic) {
reloadTopics: (function() {
var posts, topic,
_this = this;
topic = this.get('content');
if (!topic) return;
posts = topic.get('posts');
if (!posts) return;
posts.clear();
this.set('content.loaded', false);
return Discourse.Topic.find(this.get('content.id'), this.get('postFilters')).then(function(result) {
var first;
first = result.posts.first();
if (first) {
_this.set('currentPost', first.post_number);
}
$('#topic-progress .solid').data('progress', false);
result.posts.each(function(p) {
return posts.pushObject(Discourse.Post.create(p, topic));
});
return _this.set('content.loaded', true);
});
}).observes('postFilters'),
deleteTopic: function(e) {
var _this = this;
this.unsubscribe();
this.get('content')["delete"](function() {
_this.set('message', "The topic has been deleted");
_this.set('loaded', false);
});
},
toggleVisibility: function() {
this.get('content').toggleStatus('visible');
},
toggleClosed: function() {
this.get('content').toggleStatus('closed');
},
togglePinned: function() {
this.get('content').toggleStatus('pinned');
},
toggleArchived: function() {
this.get('content').toggleStatus('archived');
},
convertToRegular: function() {
this.get('content').convertArchetype('regular');
},
startTracking: function() {
var screenTrack;
screenTrack = Discourse.ScreenTrack.create({ topic_id: this.get('content.id') });
screenTrack.start();
return this.set('content.screenTrack', screenTrack);
},
stopTracking: function() {
var screenTrack = this.get('content.screenTrack');
if (screenTrack) screenTrack.stop();
this.set('content.screenTrack', null);
},
// Toggle the star on the topic
toggleStar: function(e) {
this.get('content').toggleStar();
},
// Receive notifications for this topic
subscribe: function() {
var bus,
_this = this;
bus = Discourse.MessageBus;
// there is a condition where the view never calls unsubscribe, navigate to a topic from a topic
bus.unsubscribe('/topic/*');
return bus.subscribe("/topic/" + (this.get('content.id')), function(data) {
var posts, topic;
topic = _this.get('content');
if (data.notification_level_change) {
topic.set('notification_level', data.notification_level_change);
topic.set('notifications_reason_id', data.notifications_reason_id);
return; return;
} }
posts = topic.get('posts'); posts = topic.get('posts');
if (!posts) { if (posts.some(function(p) {
return p.get('post_number') === data.post_number;
})) {
return; return;
} }
posts.clear(); topic.set('posts_count', topic.get('posts_count') + 1);
this.set('content.loaded', false); topic.set('highest_post_number', data.post_number);
return Discourse.Topic.find(this.get('content.id'), this.get('postFilters')).then(function(result) { topic.set('last_poster', data.user);
var first; topic.set('last_posted_at', data.created_at);
first = result.posts.first(); return Discourse.notifyTitle();
if (first) { });
_this.set('currentPost', first.post_number); },
}
jQuery('#topic-progress .solid').data('progress', false);
result.posts.each(function(p) {
return posts.pushObject(Discourse.Post.create(p, topic));
});
return _this.set('content.loaded', true);
});
}).observes('postFilters'),
deleteTopic: function(e) {
var _this = this;
this.unsubscribe();
return this.get('content')["delete"](function() {
_this.set('message', "The topic has been deleted");
return _this.set('loaded', false);
});
},
toggleVisibility: function() {
return this.get('content').toggleStatus('visible');
},
toggleClosed: function() {
return this.get('content').toggleStatus('closed');
},
togglePinned: function() {
return this.get('content').toggleStatus('pinned');
},
toggleArchived: function() {
return this.get('content').toggleStatus('archived');
},
convertToRegular: function() {
return this.get('content').convertArchetype('regular');
},
startTracking: function() {
var screenTrack;
screenTrack = Discourse.ScreenTrack.create({
topic_id: this.get('content.id')
});
screenTrack.start();
return this.set('content.screenTrack', screenTrack);
},
stopTracking: function() {
var _ref;
if (_ref = this.get('content.screenTrack')) {
_ref.stop();
}
return this.set('content.screenTrack', null);
},
/* Toggle the star on the topic
*/
toggleStar: function(e) { unsubscribe: function() {
return this.get('content').toggleStar(); var bus, topicId;
}, topicId = this.get('content.id');
/* Receive notifications for this topic if (!topicId) {
*/ return;
}
bus = Discourse.MessageBus;
return bus.unsubscribe("/topic/" + topicId);
},
subscribe: function() { // Post related methods
var bus, replyToPost: function(post) {
_this = this; var composerController, promise, quoteController, quotedText,
bus = Discourse.MessageBus; _this = this;
/* there is a condition where the view never calls unsubscribe, navigate to a topic from a topic composerController = this.get('controllers.composer');
*/ quoteController = this.get('controllers.quoteButton');
quotedText = Discourse.BBCode.buildQuoteBBCode(quoteController.get('post'), quoteController.get('buffer'));
bus.unsubscribe('/topic/*'); quoteController.set('buffer', '');
return bus.subscribe("/topic/" + (this.get('content.id')), function(data) { if (composerController.get('content.topic.id') === post.get('topic.id') && composerController.get('content.action') === Discourse.Composer.REPLY) {
var posts, topic; composerController.set('content.post', post);
topic = _this.get('content'); composerController.set('content.composeState', Discourse.Composer.OPEN);
if (data.notification_level_change) { composerController.appendText(quotedText);
topic.set('notification_level', data.notification_level_change); } else {
topic.set('notifications_reason_id', data.notifications_reason_id); promise = composerController.open({
return;
}
posts = topic.get('posts');
if (posts.some(function(p) {
return p.get('post_number') === data.post_number;
})) {
return;
}
topic.set('posts_count', topic.get('posts_count') + 1);
topic.set('highest_post_number', data.post_number);
topic.set('last_poster', data.user);
topic.set('last_posted_at', data.created_at);
return Discourse.notifyTitle();
});
},
unsubscribe: function() {
var bus, topicId;
topicId = this.get('content.id');
if (!topicId) {
return;
}
bus = Discourse.MessageBus;
return bus.unsubscribe("/topic/" + topicId);
},
/* Post related methods
*/
replyToPost: function(post) {
var composerController, promise, quoteController, quotedText,
_this = this;
composerController = this.get('controllers.composer');
quoteController = this.get('controllers.quoteButton');
quotedText = Discourse.BBCode.buildQuoteBBCode(quoteController.get('post'), quoteController.get('buffer'));
quoteController.set('buffer', '');
if (composerController.get('content.topic.id') === post.get('topic.id') && composerController.get('content.action') === Discourse.Composer.REPLY) {
composerController.set('content.post', post);
composerController.set('content.composeState', Discourse.Composer.OPEN);
composerController.appendText(quotedText);
} else {
promise = composerController.open({
post: post,
action: Discourse.Composer.REPLY,
draftKey: post.get('topic.draft_key'),
draftSequence: post.get('topic.draft_sequence')
});
promise.then(function() {
return composerController.appendText(quotedText);
});
}
return false;
},
/* Edits a post
*/
editPost: function(post) {
return this.get('controllers.composer').open({
post: post, post: post,
action: Discourse.Composer.EDIT, action: Discourse.Composer.REPLY,
draftKey: post.get('topic.draft_key'), draftKey: post.get('topic.draft_key'),
draftSequence: post.get('topic.draft_sequence') draftSequence: post.get('topic.draft_sequence')
}); });
}, promise.then(function() { return composerController.appendText(quotedText); });
toggleBookmark: function(post) {
if (!Discourse.get('currentUser')) {
alert(Em.String.i18n("bookmarks.not_bookmarked"));
return;
}
post.toggleProperty('bookmarked');
return false;
},
clearFlags: function(actionType) {
return actionType.clearFlags();
},
/* Who acted on a particular post / action type
*/
whoActed: function(actionType) {
actionType.loadUsers();
return false;
},
showPrivateInviteModal: function() {
var modal, _ref;
modal = Discourse.InvitePrivateModalView.create({
topic: this.get('content')
});
if (_ref = this.get('controllers.modal')) {
_ref.show(modal);
}
return false;
},
showInviteModal: function() {
var _ref;
if (_ref = this.get('controllers.modal')) {
_ref.show(Discourse.InviteModalView.create({
topic: this.get('content')
}));
}
return false;
},
// Clicked the flag button
showFlags: function(post) {
var flagView, _ref;
flagView = Discourse.FlagView.create({
post: post,
controller: this
});
return (_ref = this.get('controllers.modal')) ? _ref.show(flagView) : void 0;
},
showHistory: function(post) {
var view, _ref;
view = Discourse.HistoryView.create({
originalPost: post
});
if (_ref = this.get('controllers.modal')) {
_ref.show(view);
}
return false;
},
recoverPost: function(post) {
post.set('deleted_at', null);
return post.recover();
},
deletePost: function(post) {
/* Moderators can delete posts. Regular users can only create a deleted at message.
*/
if (Discourse.get('currentUser.moderator')) {
post.set('deleted_at', new Date());
} else {
post.set('cooked', Discourse.Utilities.cook(Em.String.i18n("post.deleted_by_author")));
post.set('can_delete', false);
post.set('version', post.get('version') + 1);
}
return post["delete"]();
} }
}); return false;
},
// Edits a post
editPost: function(post) {
return this.get('controllers.composer').open({
post: post,
action: Discourse.Composer.EDIT,
draftKey: post.get('topic.draft_key'),
draftSequence: post.get('topic.draft_sequence')
});
},
toggleBookmark: function(post) {
if (!Discourse.get('currentUser')) {
alert(Em.String.i18n("bookmarks.not_bookmarked"));
return;
}
post.toggleProperty('bookmarked');
return false;
},
clearFlags: function(actionType) {
actionType.clearFlags();
},
// Who acted on a particular post / action type
whoActed: function(actionType) {
actionType.loadUsers();
return false;
},
showPrivateInviteModal: function() {
var modal, _ref;
modal = Discourse.InvitePrivateModalView.create({
topic: this.get('content')
});
if (_ref = this.get('controllers.modal')) {
_ref.show(modal);
}
return false;
},
showInviteModal: function() {
var _ref;
if (_ref = this.get('controllers.modal')) {
_ref.show(Discourse.InviteModalView.create({
topic: this.get('content')
}));
}
return false;
},
// Clicked the flag button
showFlags: function(post) {
var flagView, _ref;
flagView = Discourse.FlagView.create({
post: post,
controller: this
});
return (_ref = this.get('controllers.modal')) ? _ref.show(flagView) : void 0;
},
showHistory: function(post) {
var view, _ref;
view = Discourse.HistoryView.create({ originalPost: post });
if (_ref = this.get('controllers.modal')) {
_ref.show(view);
}
return false;
},
recoverPost: function(post) {
post.set('deleted_at', null);
return post.recover();
},
deletePost: function(post) {
// Moderators can delete posts. Regular users can only create a deleted at message.
if (Discourse.get('currentUser.moderator')) {
post.set('deleted_at', new Date());
} else {
post.set('cooked', Discourse.Utilities.cook(Em.String.i18n("post.deleted_by_author")));
post.set('can_delete', false);
post.set('version', post.get('version') + 1);
}
return post["delete"]();
}
});
}).call(this);

View File

@ -1,20 +1,28 @@
(function() { /**
This controller supports all actions on a user's activity stream
Discourse.UserActivityController = Ember.ObjectController.extend({ @class UserActivityController
needs: ['composer'], @extends Discourse.ObjectController
kickOffPrivateMessage: (function() { @namespace Discourse
if (this.get('content.openPrivateMessage')) { @module Discourse
return this.composePrivateMessage(); **/
} Discourse.UserActivityController = Discourse.ObjectController.extend({
}).observes('content.openPrivateMessage'), needs: ['composer'],
composePrivateMessage: function() {
return this.get('controllers.composer').open({ kickOffPrivateMessage: (function() {
action: Discourse.Composer.PRIVATE_MESSAGE, if (this.get('content.openPrivateMessage')) {
usernames: this.get('content').username, this.composePrivateMessage();
archetypeId: 'private_message',
draftKey: 'new_private_message'
});
} }
}); }).observes('content.openPrivateMessage'),
composePrivateMessage: function() {
return this.get('controllers.composer').open({
action: Discourse.Composer.PRIVATE_MESSAGE,
usernames: this.get('content').username,
archetypeId: 'private_message',
draftKey: 'new_private_message'
});
}
});
}).call(this);

View File

@ -1,12 +1,21 @@
(function() { /**
This controller handles general user actions
@class UserController
@extends Discourse.ObjectController
@namespace Discourse
@module Discourse
**/
Discourse.UserController = Discourse.ObjectController.extend({
viewingSelf: (function() {
return this.get('content.username') === Discourse.get('currentUser.username');
}).property('content.username', 'Discourse.currentUser.username'),
canSeePrivateMessages: (function() {
return this.get('viewingSelf') || Discourse.get('currentUser.admin');
}).property('viewingSelf', 'Discourse.currentUser')
});
Discourse.UserController = Ember.ObjectController.extend({
viewingSelf: (function() {
return this.get('content.username') === Discourse.get('currentUser.username');
}).property('content.username', 'Discourse.currentUser.username'),
canSeePrivateMessages: (function() {
return this.get('viewingSelf') || Discourse.get('currentUser.admin');
}).property('viewingSelf', 'Discourse.currentUser')
});
}).call(this);

View File

@ -1,10 +1,16 @@
(function() { /**
This controller handles actions related to a user's invitations
@class UserInvitedController
@extends Discourse.ObjectController
@namespace Discourse
@module Discourse
**/
Discourse.UserInvitedController = Discourse.ObjectController.extend({
rescind: function(invite) {
invite.rescind();
return false;
}
});
Discourse.UserInvitedController = Ember.ObjectController.extend({
rescind: function(invite) {
invite.rescind();
return false;
}
});
}).call(this);

View File

@ -1,18 +1,25 @@
(function() { /**
This controller handles actions related to a user's private messages.
Discourse.UserPrivateMessagesController = Ember.ObjectController.extend({ @class UserPrivateMessagesController
editPreferences: function() { @extends Discourse.ObjectController
return Discourse.routeTo("/users/" + (this.get('content.username_lower')) + "/preferences"); @namespace Discourse
}, @module Discourse
composePrivateMessage: function() { **/
var composerController; Discourse.UserPrivateMessagesController = Discourse.ObjectController.extend({
composerController = Discourse.get('router.composerController');
return composerController.open({
action: Discourse.Composer.PRIVATE_MESSAGE,
archetypeId: 'private_message',
draftKey: 'new_private_message'
});
}
});
}).call(this); editPreferences: function() {
return Discourse.routeTo("/users/" + (this.get('content.username_lower')) + "/preferences");
},
composePrivateMessage: function() {
var composerController;
composerController = Discourse.get('router.composerController');
return composerController.open({
action: Discourse.Composer.PRIVATE_MESSAGE,
archetypeId: 'private_message',
draftKey: 'new_private_message'
});
}
});

View File

@ -1,190 +1,260 @@
/*global humaneDate:true */ /*global humaneDate:true */
(function() { /**
Breaks up a long string
Handlebars.registerHelper('breakUp', function(property, options) { @method breakUp
var prop, result, tokens; @for Handlebars
prop = Ember.Handlebars.get(this, property, options); **/
if (!prop) { Handlebars.registerHelper('breakUp', function(property, options) {
return ""; var prop, result, tokens;
} prop = Ember.Handlebars.get(this, property, options);
tokens = prop.match(new RegExp(".{1,14}", 'g')); if (!prop) {
if (tokens.length === 1) { return "";
return prop; }
} tokens = prop.match(new RegExp(".{1,14}", 'g'));
result = ""; if (tokens.length === 1) {
tokens.each(function(token, index) { return prop;
result += token; }
if (token.indexOf(' ') === -1 && (index < tokens.length - 1)) { result = "";
result += "- "; tokens.each(function(token, index) {
} result += token;
}); if (token.indexOf(' ') === -1 && (index < tokens.length - 1)) {
return result; result += "- ";
});
Handlebars.registerHelper('shorten', function(property, options) {
var str;
str = Ember.Handlebars.get(this, property, options);
return str.truncate(35);
});
Handlebars.registerHelper('topicLink', function(property, options) {
var title, topic;
topic = Ember.Handlebars.get(this, property, options);
title = topic.get('fancy_title') || topic.get('title');
return "<a href='" + (topic.get('lastReadUrl')) + "' class='title excerptable'>" + title + "</a>";
});
Handlebars.registerHelper('categoryLink', function(property, options) {
var category;
category = Ember.Handlebars.get(this, property, options);
return new Handlebars.SafeString(Discourse.Utilities.categoryLink(category));
});
Handlebars.registerHelper('titledLinkTo', function(name, object) {
var options;
options = [].slice.call(arguments, -1)[0];
if (options.hash.titleKey) {
options.hash.title = Em.String.i18n(options.hash.titleKey);
}
if (arguments.length === 3) {
return Ember.Handlebars.helpers.linkTo.call(this, name, object, options);
} else {
return Ember.Handlebars.helpers.linkTo.call(this, name, options);
} }
}); });
return result;
});
Handlebars.registerHelper('shortenUrl', function(property, options) { /**
var url; Truncates long strings
url = Ember.Handlebars.get(this, property, options);
/* Remove trailing slash if it's a top level URL
*/
if (url.match(/\//g).length === 3) { @method shorten
url = url.replace(/\/$/, ''); @for Handlebars
} **/
url = url.replace(/^https?:\/\//, ''); Handlebars.registerHelper('shorten', function(property, options) {
url = url.replace(/^www\./, ''); return Ember.Handlebars.get(this, property, options).truncate(35);
return url.truncate(80); });
});
Handlebars.registerHelper('lower', function(property, options) { /**
var o; Produces a link to a topic
o = Ember.Handlebars.get(this, property, options);
if (o && typeof o === 'string') {
return o.toLowerCase();
} else {
return "";
}
});
Handlebars.registerHelper('avatar', function(user, options) { @method topicLink
var title, username; @for Handlebars
if (typeof user === 'string') { **/
user = Ember.Handlebars.get(this, user, options); Handlebars.registerHelper('topicLink', function(property, options) {
} var title, topic;
username = Em.get(user, 'username'); topic = Ember.Handlebars.get(this, property, options);
if (!username) { title = topic.get('fancy_title') || topic.get('title');
username = Em.get(user, options.hash.usernamePath); return "<a href='" + (topic.get('lastReadUrl')) + "' class='title'>" + title + "</a>";
} });
if (!options.hash.ignoreTitle) {
title = Em.get(user, 'title') || Em.get(user, 'description');
}
return new Handlebars.SafeString(Discourse.Utilities.avatarImg({
size: options.hash.imageSize,
extraClasses: Em.get(user, 'extras') || options.hash.extraClasses,
username: username,
title: title || username,
avatarTemplate: Ember.get(user, 'avatar_template') || options.hash.avatarTemplate
}));
});
Handlebars.registerHelper('unboundDate', function(property, options) { /**
var dt; Produces a link to a category
dt = new Date(Ember.Handlebars.get(this, property, options));
@method categoryLink
@for Handlebars
**/
Handlebars.registerHelper('categoryLink', function(property, options) {
var category;
category = Ember.Handlebars.get(this, property, options);
return new Handlebars.SafeString(Discourse.Utilities.categoryLink(category));
});
/**
Produces a link to a route with support for i18n on the title
@method titledLinkTo
@for Handlebars
**/
Handlebars.registerHelper('titledLinkTo', function(name, object) {
var options;
options = [].slice.call(arguments, -1)[0];
if (options.hash.titleKey) {
options.hash.title = Em.String.i18n(options.hash.titleKey);
}
if (arguments.length === 3) {
return Ember.Handlebars.helpers.linkTo.call(this, name, object, options);
} else {
return Ember.Handlebars.helpers.linkTo.call(this, name, options);
}
});
/**
Shorten a URL for display by removing common components
@method shortenUrl
@for Handlebars
**/
Handlebars.registerHelper('shortenUrl', function(property, options) {
var url;
url = Ember.Handlebars.get(this, property, options);
// Remove trailing slash if it's a top level URL
if (url.match(/\//g).length === 3) {
url = url.replace(/\/$/, '');
}
url = url.replace(/^https?:\/\//, '');
url = url.replace(/^www\./, '');
return url.truncate(80);
});
/**
Display a property in lower case
@method lower
@for Handlebars
**/
Handlebars.registerHelper('lower', function(property, options) {
var o;
o = Ember.Handlebars.get(this, property, options);
if (o && typeof o === 'string') {
return o.toLowerCase();
} else {
return "";
}
});
/**
Show an avatar for a user, intelligently making use of available properties
@method avatar
@for Handlebars
**/
Handlebars.registerHelper('avatar', function(user, options) {
var title, username;
if (typeof user === 'string') {
user = Ember.Handlebars.get(this, user, options);
}
username = Em.get(user, 'username');
if (!username) {
username = Em.get(user, options.hash.usernamePath);
}
if (!options.hash.ignoreTitle) {
title = Em.get(user, 'title') || Em.get(user, 'description');
}
return new Handlebars.SafeString(Discourse.Utilities.avatarImg({
size: options.hash.imageSize,
extraClasses: Em.get(user, 'extras') || options.hash.extraClasses,
username: username,
title: title || username,
avatarTemplate: Ember.get(user, 'avatar_template') || options.hash.avatarTemplate
}));
});
/**
Nicely format a date without a binding since the date doesn't need to change.
@method unboundDate
@for Handlebars
**/
Handlebars.registerHelper('unboundDate', function(property, options) {
var dt;
dt = new Date(Ember.Handlebars.get(this, property, options));
return dt.format("{d} {Mon}, {yyyy} {hh}:{mm}");
});
/**
Display a date related to an edit of a post
@method editDate
@for Handlebars
**/
Handlebars.registerHelper('editDate', function(property, options) {
var dt, yesterday;
dt = Date.create(Ember.Handlebars.get(this, property, options));
yesterday = new Date() - (60 * 60 * 24 * 1000);
if (yesterday > dt.getTime()) {
return dt.format("{d} {Mon}, {yyyy} {hh}:{mm}"); return dt.format("{d} {Mon}, {yyyy} {hh}:{mm}");
}); } else {
return humaneDate(dt);
}
});
Handlebars.registerHelper('editDate', function(property, options) { /**
var dt, yesterday; Display logic for numbers.
dt = Date.create(Ember.Handlebars.get(this, property, options));
yesterday = new Date() - (60 * 60 * 24 * 1000); @method number
if (yesterday > dt.getTime()) { @for Handlebars
return dt.format("{d} {Mon}, {yyyy} {hh}:{mm}"); **/
Handlebars.registerHelper('number', function(property, options) {
var n, orig, title;
orig = parseInt(Ember.Handlebars.get(this, property, options), 10);
if (isNaN(orig)) {
orig = 0;
}
title = orig;
if (options.hash.numberKey) {
title = Em.String.i18n(options.hash.numberKey, {
number: orig
});
}
// Round off the thousands to one decimal place
n = orig;
if (orig > 999) {
n = (orig / 1000).toFixed(1) + "K";
}
return new Handlebars.SafeString("<span class='number' title='" + title + "'>" + n + "</span>");
});
/**
Display logic for dates.
@method date
@for Handlebars
**/
Handlebars.registerHelper('date', function(property, options) {
var displayDate, dt, fiveDaysAgo, fullReadable, humanized, leaveAgo, val;
if (property.hash) {
if (property.hash.leaveAgo) {
leaveAgo = property.hash.leaveAgo === "true";
}
if (property.hash.path) {
property = property.hash.path;
}
}
val = Ember.Handlebars.get(this, property, options);
if (!val) {
return new Handlebars.SafeString("&mdash;");
}
dt = new Date(val);
fullReadable = dt.format("{d} {Mon}, {yyyy} {hh}:{mm}");
displayDate = "";
fiveDaysAgo = (new Date()) - 432000000;
if (fiveDaysAgo > (dt.getTime())) {
if ((new Date()).getFullYear() !== dt.getFullYear()) {
displayDate = dt.format("{d} {Mon} '{yy}");
} else { } else {
return humaneDate(dt); displayDate = dt.format("{d} {Mon}");
} }
}); } else {
humanized = humaneDate(dt);
if (!humanized) {
return "";
}
displayDate = humanized;
if (!leaveAgo) {
displayDate = displayDate.replace(' ago', '');
}
}
return new Handlebars.SafeString("<span class='date' title='" + fullReadable + "'>" + displayDate + "</span>");
});
Handlebars.registerHelper('number', function(property, options) { /**
var n, orig, title; A personalized name for display
orig = parseInt(Ember.Handlebars.get(this, property, options), 10);
if (isNaN(orig)) {
orig = 0;
}
title = orig;
if (options.hash.numberKey) {
title = Em.String.i18n(options.hash.numberKey, {
number: orig
});
}
/* Round off the thousands to one decimal place
*/
n = orig; @method personalizedName
if (orig > 999) { @for Handlebars
n = (orig / 1000).toFixed(1) + "K"; **/
} Handlebars.registerHelper('personalizedName', function(property, options) {
return new Handlebars.SafeString("<span class='number' title='" + title + "'>" + n + "</span>"); var name, username;
}); name = Ember.Handlebars.get(this, property, options);
if (options.hash.usernamePath) {
username = Ember.Handlebars.get(this, options.hash.usernamePath, options);
}
if (username !== Discourse.get('currentUser.username')) {
return name;
}
return Em.String.i18n('you');
});
Handlebars.registerHelper('date', function(property, options) {
var displayDate, dt, fiveDaysAgo, fullReadable, humanized, leaveAgo, val;
if (property.hash) {
if (property.hash.leaveAgo) {
leaveAgo = property.hash.leaveAgo === "true";
}
if (property.hash.path) {
property = property.hash.path;
}
}
val = Ember.Handlebars.get(this, property, options);
if (!val) {
return new Handlebars.SafeString("&mdash;");
}
dt = new Date(val);
fullReadable = dt.format("{d} {Mon}, {yyyy} {hh}:{mm}");
displayDate = "";
fiveDaysAgo = (new Date()) - 432000000;
if (fiveDaysAgo > (dt.getTime())) {
if ((new Date()).getFullYear() !== dt.getFullYear()) {
displayDate = dt.format("{d} {Mon} '{yy}");
} else {
displayDate = dt.format("{d} {Mon}");
}
} else {
humanized = humaneDate(dt);
if (!humanized) {
return "";
}
displayDate = humanized;
if (!leaveAgo) {
displayDate = displayDate.replace(' ago', '');
}
}
return new Handlebars.SafeString("<span class='date' title='" + fullReadable + "'>" + displayDate + "</span>");
});
Handlebars.registerHelper('personalizedName', function(property, options) {
var name, username;
name = Ember.Handlebars.get(this, property, options);
if (options.hash.usernamePath) {
username = Ember.Handlebars.get(this, options.hash.usernamePath, options);
}
if (username !== Discourse.get('currentUser.username')) {
return name;
}
return Em.String.i18n('you');
});
}).call(this);

View File

@ -1,50 +1,59 @@
(function() { /**
Look up a translation for an i18n key in our dictionary.
Ember.Handlebars.registerHelper('i18n', function(property, options) { @method i18n
/* Resolve any properties @for Handlebars
*/ **/
Ember.Handlebars.registerHelper('i18n', function(property, options) {
var params, // Resolve any properties
_this = this; var params,
params = options.hash; _this = this;
Object.keys(params, function(key, value) { params = options.hash;
params[key] = Em.Handlebars.get(_this, value, options); Object.keys(params, function(key, value) {
}); params[key] = Em.Handlebars.get(_this, value, options);
return Ember.String.i18n(property, params);
}); });
return Ember.String.i18n(property, params);
});
/* We always prefix with .js to select exactly what we want passed through to the front end. /* We always prefix with .js to select exactly what we want passed through to the front end.
*/ */
/**
Look up a translation for an i18n key in our dictionary.
Ember.String.i18n = function(scope, options) { @method i18n
return I18n.translate("js." + scope, options); @for Ember.String
**/
Ember.String.i18n = function(scope, options) {
return I18n.translate("js." + scope, options);
};
/**
Set up an i18n binding that will update as a count changes, complete with pluralization.
@method countI18n
@for Handlebars
**/
Ember.Handlebars.registerHelper('countI18n', function(key, options) {
var view;
view = Discourse.View.extend({
tagName: 'span',
render: function(buffer) {
return buffer.push(Ember.String.i18n(key, {
count: this.get('count')
}));
},
countChanged: (function() {
return this.rerender();
}).observes('count')
});
return Ember.Handlebars.helpers.view.call(this, view, options);
});
if (Ember.EXTEND_PROTOTYPES) {
String.prototype.i18n = function(options) {
return Ember.String.i18n(String(this), options);
}; };
}
/* Bind an i18n count
*/
Ember.Handlebars.registerHelper('countI18n', function(key, options) {
var view;
view = Discourse.View.extend({
tagName: 'span',
render: function(buffer) {
return buffer.push(Ember.String.i18n(key, {
count: this.get('count')
}));
},
countChanged: (function() {
return this.rerender();
}).observes('count')
});
return Ember.Handlebars.helpers.view.call(this, view, options);
});
if (Ember.EXTEND_PROTOTYPES) {
String.prototype.i18n = function(options) {
return Ember.String.i18n(String(this), options);
};
}
}).call(this);

View File

@ -1,48 +1,46 @@
(function() { /**
This mixin provides `blank` and `present` to determine whether properties are
there, accounting for more cases than just null and undefined.
@class Discourse.Presence
@extends Ember.Mixin
@namespace Discourse
@module Discourse
**/
Discourse.Presence = Em.Mixin.create({
/** /**
This mixin provides `blank` and `present` to determine whether properties are Returns whether a property is blank. It considers empty arrays, string, objects, undefined and null
there, accounting for more cases than just null and undefined. to be blank, otherwise true.
@class Discourse.Presence @method blank
@extends Ember.Mixin @param {String} name the name of the property we want to check
@namespace Discourse @return {Boolean}
@module Discourse */
**/ blank: function(name) {
window.Discourse.Presence = Em.Mixin.create({ var prop;
prop = this.get(name);
if (!prop) return true;
/** switch (typeof prop) {
Returns whether a property is blank. It considers empty arrays, string, objects, undefined and null case "string":
to be blank, otherwise true. return prop.trim().isBlank();
case "object":
@method blank return Object.isEmpty(prop);
@param {String} name the name of the property we want to check
@return {Boolean}
*/
blank: function(name) {
var prop;
prop = this.get(name);
if (!prop) return true;
switch (typeof prop) {
case "string":
return prop.trim().isBlank();
case "object":
return Object.isEmpty(prop);
}
return false;
},
/**
Returns whether a property is present. A present property is the opposite of a `blank` one.
@method present
@param {String} name the name of the property we want to check
@return {Boolean}
*/
present: function(name) {
return !this.blank(name);
} }
}); return false;
},
/**
Returns whether a property is present. A present property is the opposite of a `blank` one.
@method present
@param {String} name the name of the property we want to check
@return {Boolean}
*/
present: function(name) {
return !this.blank(name);
}
});
}).call(this);

View File

@ -1,24 +1,37 @@
/**
This mixin adds support for being notified every time the browser window
is scrolled.
/* Use this mixin if you want to be notified every time the user scrolls the window @class Discourse.Scrolling
*/ @extends Ember.Mixin
@namespace Discourse
@module Discourse
**/
Discourse.Scrolling = Em.Mixin.create({
/**
Begin watching for scroll events. They will be called at max every 100ms.
@method bindScrolling
*/
bindScrolling: function() {
var onScroll,
_this = this;
onScroll = Discourse.debounce(function() { return _this.scrolled(); }, 100);
$(document).bind('touchmove.discourse', onScroll);
$(window).bind('scroll.discourse', onScroll);
},
/**
Begin watching for scroll events. They will be called at max every 100ms.
@method unbindScrolling
*/
unbindScrolling: function() {
$(window).unbind('scroll.discourse');
$(document).unbind('touchmove.discourse');
}
});
(function() {
window.Discourse.Scrolling = Em.Mixin.create({
bindScrolling: function() {
var onScroll,
_this = this;
onScroll = Discourse.debounce(function() {
return _this.scrolled();
}, 100);
jQuery(document).bind('touchmove.discourse', onScroll);
return jQuery(window).bind('scroll.discourse', onScroll);
},
unbindScrolling: function() {
jQuery(window).unbind('scroll.discourse');
return jQuery(document).unbind('touchmove.discourse');
}
});
}).call(this);

View File

@ -1,123 +1,123 @@
(function() { /**
A data model for summarizing actions a user has taken, for example liking a post.
window.Discourse.ActionSummary = Discourse.Model.extend({ @class ActionSummary
/* Description for the action @extends Discourse.Model
*/ @namespace Discourse
@module Discourse
**/
Discourse.ActionSummary = Discourse.Model.extend({
description: (function() { // Description for the action
if (this.get('acted')) { description: (function() {
return Em.String.i18n('post.actions.by_you_and_others', { if (this.get('acted')) {
count: this.get('count') - 1, return Em.String.i18n('post.actions.by_you_and_others', {
long_form: this.get('actionType.long_form') count: this.get('count') - 1,
}); long_form: this.get('actionType.long_form')
} else {
return Em.String.i18n('post.actions.by_others', {
count: this.get('count'),
long_form: this.get('actionType.long_form')
});
}
}).property('count', 'acted', 'actionType'),
canAlsoAction: (function() {
if (this.get('hidden')) {
return false;
}
return this.get('can_act');
}).property('can_act', 'hidden'),
/* Remove it
*/
removeAction: function() {
this.set('acted', false);
this.set('count', this.get('count') - 1);
this.set('can_act', true);
return this.set('can_undo', false);
},
/* Perform this action
*/
act: function(opts) {
/* Mark it as acted
*/
var promise,
_this = this;
this.set('acted', true);
this.set('count', this.get('count') + 1);
this.set('can_act', false);
this.set('can_undo', true);
/* Add ourselves to the users who liked it if present
*/
if (this.present('users')) {
this.users.pushObject(Discourse.get('currentUser'));
}
/* Create our post action
*/
promise = new RSVP.Promise();
jQuery.ajax({
url: "/post_actions",
type: 'POST',
data: {
id: this.get('post.id'),
post_action_type_id: this.get('id'),
message: (opts ? opts.message : void 0) || ""
},
error: function(error) {
var errors;
_this.removeAction();
errors = jQuery.parseJSON(error.responseText).errors;
return promise.reject(errors);
},
success: function() {
return promise.resolve();
}
}); });
return promise; } else {
}, return Em.String.i18n('post.actions.by_others', {
/* Undo this action count: this.get('count'),
*/ long_form: this.get('actionType.long_form')
undo: function() {
this.removeAction();
/* Remove our post action
*/
return jQuery.ajax({
url: "/post_actions/" + (this.get('post.id')),
type: 'DELETE',
data: {
post_action_type_id: this.get('id')
}
});
},
clearFlags: function() {
var _this = this;
return jQuery.ajax({
url: "/post_actions/clear_flags",
type: "POST",
data: {
post_action_type_id: this.get('id'),
id: this.get('post.id')
},
success: function(result) {
_this.set('post.hidden', result.hidden);
return _this.set('count', 0);
}
});
},
loadUsers: function() {
var _this = this;
return jQuery.getJSON("/post_actions/users", {
id: this.get('post.id'),
post_action_type_id: this.get('id')
}, function(result) {
_this.set('users', Em.A());
return result.each(function(u) {
return _this.get('users').pushObject(Discourse.User.create(u));
});
}); });
} }
}); }).property('count', 'acted', 'actionType'),
canAlsoAction: (function() {
if (this.get('hidden')) return false;
return this.get('can_act');
}).property('can_act', 'hidden'),
// Remove it
removeAction: function() {
this.set('acted', false);
this.set('count', this.get('count') - 1);
this.set('can_act', true);
return this.set('can_undo', false);
},
// Perform this action
act: function(opts) {
// Mark it as acted
var promise,
_this = this;
this.set('acted', true);
this.set('count', this.get('count') + 1);
this.set('can_act', false);
this.set('can_undo', true);
// Add ourselves to the users who liked it if present
if (this.present('users')) {
this.users.pushObject(Discourse.get('currentUser'));
}
// Create our post action
promise = new RSVP.Promise();
jQuery.ajax({
url: "/post_actions",
type: 'POST',
data: {
id: this.get('post.id'),
post_action_type_id: this.get('id'),
message: (opts ? opts.message : void 0) || ""
},
error: function(error) {
var errors;
_this.removeAction();
errors = jQuery.parseJSON(error.responseText).errors;
return promise.reject(errors);
},
success: function() {
return promise.resolve();
}
});
return promise;
},
// Undo this action
undo: function() {
this.removeAction();
// Remove our post action
return jQuery.ajax({
url: "/post_actions/" + (this.get('post.id')),
type: 'DELETE',
data: {
post_action_type_id: this.get('id')
}
});
},
clearFlags: function() {
var _this = this;
return jQuery.ajax({
url: "/post_actions/clear_flags",
type: "POST",
data: {
post_action_type_id: this.get('id'),
id: this.get('post.id')
},
success: function(result) {
_this.set('post.hidden', result.hidden);
return _this.set('count', 0);
}
});
},
loadUsers: function() {
var _this = this;
return jQuery.getJSON("/post_actions/users", {
id: this.get('post.id'),
post_action_type_id: this.get('id')
}, function(result) {
_this.set('users', Em.A());
return result.each(function(u) {
return _this.get('users').pushObject(Discourse.User.create(u));
});
});
}
});
}).call(this);

View File

@ -1,15 +1,22 @@
(function() { /**
A data model for archetypes such as polls, tasks, etc.
@class Archetype
@extends Discourse.Model
@namespace Discourse
@module Discourse
**/
Discourse.Archetype = Discourse.Model.extend({
hasOptions: (function() {
if (!this.get('options')) return false;
return this.get('options').length > 0;
}).property('options.@each'),
isDefault: (function() {
return this.get('id') === Discourse.get('site.default_archetype');
}).property('id')
});
window.Discourse.Archetype = Discourse.Model.extend({
hasOptions: (function() {
if (!this.get('options')) {
return false;
}
return this.get('options').length > 0;
}).property('options.@each'),
isDefault: (function() {
return this.get('id') === Discourse.get('site.default_archetype');
}).property('id')
});
}).call(this);

View File

@ -1,45 +1,55 @@
(function() { /**
A data model that represents a category
window.Discourse.Category = Discourse.Model.extend({ @class Category
url: (function() { @extends Discourse.Model
return "/category/" + (this.get('slug')); @namespace Discourse
}).property('name'), @module Discourse
style: (function() { **/
return "background-color: #" + (this.get('color')); Discourse.Category = Discourse.Model.extend({
}).property('color'),
moreTopics: (function() { url: (function() {
return this.get('topic_count') > Discourse.SiteSettings.category_featured_topics; return "/category/" + (this.get('slug'));
}).property('topic_count'), }).property('name'),
save: function(args) {
var url, style: (function() {
_this = this; return "background-color: #" + (this.get('color'));
url = "/categories"; }).property('color'),
if (this.get('id')) {
url = "/categories/" + (this.get('id')); moreTopics: (function() {
} return this.get('topic_count') > Discourse.SiteSettings.category_featured_topics;
return this.ajax(url, { }).property('topic_count'),
data: {
name: this.get('name'), save: function(args) {
color: this.get('color') var url,
}, _this = this;
type: this.get('id') ? 'PUT' : 'POST',
success: function(result) { url = "/categories";
return args.success(result); if (this.get('id')) {
}, url = "/categories/" + (this.get('id'));
error: function(errors) {
return args.error(errors);
}
});
},
"delete": function(callback) {
var _this = this;
return jQuery.ajax("/categories/" + (this.get('slug')), {
type: 'DELETE',
success: function() {
return callback();
}
});
} }
});
}).call(this); return this.ajax(url, {
data: {
name: this.get('name'),
color: this.get('color')
},
type: this.get('id') ? 'PUT' : 'POST',
success: function(result) { return args.success(result); },
error: function(errors) { return args.error(errors); }
});
},
"delete": function(callback) {
var _this = this;
return jQuery.ajax("/categories/" + (this.get('slug')), {
type: 'DELETE',
success: function() {
return callback();
}
});
}
});

View File

@ -1,41 +1,50 @@
(function() { /**
A data model for containing a list of categories
window.Discourse.CategoryList = Discourse.Model.extend({}); @class CategoryList
@extends Discourse.Model
@namespace Discourse
@module Discourse
**/
window.Discourse.CategoryList = Discourse.Model.extend({});
window.Discourse.CategoryList.reopenClass({
categoriesFrom: function(result) {
var categories, users;
categories = Em.A();
users = this.extractByKey(result.featured_users, Discourse.User);
result.category_list.categories.each(function(c) {
if (c.featured_user_ids) {
c.featured_users = c.featured_user_ids.map(function(u) {
return users[u];
});
}
if (c.topics) {
c.topics = c.topics.map(function(t) {
return Discourse.Topic.create(t);
});
}
return categories.pushObject(Discourse.Category.create(c));
});
return categories;
},
list: function(filter) {
var promise,
_this = this;
promise = new RSVP.Promise();
jQuery.getJSON("/" + filter + ".json").then(function(result) {
var categoryList;
categoryList = Discourse.TopicList.create();
categoryList.set('can_create_category', result.category_list.can_create_category);
categoryList.set('categories', _this.categoriesFrom(result));
categoryList.set('loaded', true);
return promise.resolve(categoryList);
});
return promise;
}
});
window.Discourse.CategoryList.reopenClass({
categoriesFrom: function(result) {
var categories, users;
categories = Em.A();
users = this.extractByKey(result.featured_users, Discourse.User);
result.category_list.categories.each(function(c) {
if (c.featured_user_ids) {
c.featured_users = c.featured_user_ids.map(function(u) {
return users[u];
});
}
if (c.topics) {
c.topics = c.topics.map(function(t) {
return Discourse.Topic.create(t);
});
}
return categories.pushObject(Discourse.Category.create(c));
});
return categories;
},
list: function(filter) {
var promise,
_this = this;
promise = new RSVP.Promise();
jQuery.getJSON("/" + filter + ".json").then(function(result) {
var categoryList;
categoryList = Discourse.TopicList.create();
categoryList.set('can_create_category', result.category_list.can_create_category);
categoryList.set('categories', _this.categoriesFrom(result));
categoryList.set('loaded', true);
return promise.resolve(categoryList);
});
return promise;
}
});
}).call(this);

File diff suppressed because it is too large Load Diff

View File

@ -1,80 +1,78 @@
(function() { /**
A data model representing a draft post
window.Discourse.Draft = Discourse.Model.extend({}); @class Draft
@extends Discourse.Model
@namespace Discourse
@module Discourse
**/
Discourse.Draft = Discourse.Model.extend({});
Discourse.Draft.reopenClass({ Discourse.Draft.reopenClass({
clear: function(key, sequence) {
return jQuery.ajax({
type: 'DELETE',
url: "/draft",
data: {
draft_key: key,
sequence: sequence
}
});
/* Discourse.KeyValueStore.remove("draft_#{key}")
*/
}, clear: function(key, sequence) {
get: function(key) { return jQuery.ajax({
var promise, type: 'DELETE',
_this = this; url: "/draft",
promise = new RSVP.Promise(); data: {
jQuery.ajax({ draft_key: key,
url: '/draft', sequence: sequence
data: { }
draft_key: key });
}, },
dataType: 'json',
success: function(data) {
return promise.resolve(data);
}
});
return promise;
},
getLocal: function(key, current) {
var local;
return current;
/* disabling for now to see if it helps with siracusa issue.
local = Discourse.KeyValueStore.get("draft_" + key);
if (!current || (local && local.length > current.length)) {
return local;
} else {
return current;
}
*/
},
save: function(key, sequence, data) {
var promise;
promise = new RSVP.Promise();
data = typeof data === "string" ? data : JSON.stringify(data);
jQuery.ajax({
type: 'POST',
url: "/draft",
data: {
draft_key: key,
data: data,
sequence: sequence
},
success: function() {
/* don't keep local
*/
/* Discourse.KeyValueStore.remove("draft_#{key}") get: function(key) {
*/ var promise,
return promise.resolve(); _this = this;
}, promise = new RSVP.Promise();
error: function() { jQuery.ajax({
/* save local url: '/draft',
*/ data: {
draft_key: key
},
dataType: 'json',
success: function(data) {
return promise.resolve(data);
}
});
return promise;
},
/* Discourse.KeyValueStore.set(key: "draft_#{key}", value: data) getLocal: function(key, current) {
*/ var local;
return promise.reject(); return current;
} },
});
return promise;
}
});
}).call(this); save: function(key, sequence, data) {
var promise;
promise = new RSVP.Promise();
data = typeof data === "string" ? data : JSON.stringify(data);
jQuery.ajax({
type: 'POST',
url: "/draft",
data: {
draft_key: key,
data: data,
sequence: sequence
},
success: function() {
/* don't keep local
*/
/* Discourse.KeyValueStore.remove("draft_#{key}")
*/
return promise.resolve();
},
error: function() {
/* save local
*/
/* Discourse.KeyValueStore.set(key: "draft_#{key}", value: data)
*/
return promise.reject();
}
});
return promise;
}
});

View File

@ -1,5 +1,11 @@
(function() { /**
A trivial model we use to handle input validation
@class InputValidation
@extends Discourse.Model
@namespace Discourse
@module Discourse
**/
window.Discourse.InputValidation = Discourse.Model.extend({});
window.Discourse.InputValidation = Discourse.Model.extend({});
}).call(this);

View File

@ -1,26 +1,35 @@
(function() { /**
A data model representing an Invite
window.Discourse.Invite = Discourse.Model.extend({ @class Invite
rescind: function() { @extends Discourse.Model
jQuery.ajax('/invites', { @namespace Discourse
type: 'DELETE', @module Discourse
data: { **/
email: this.get('email')
} Discourse.Invite = Discourse.Model.extend({
});
return this.set('rescinded', true); rescind: function() {
jQuery.ajax('/invites', {
type: 'DELETE',
data: { email: this.get('email') }
});
this.set('rescinded', true);
}
});
Discourse.Invite.reopenClass({
create: function(invite) {
var result;
result = this._super(invite);
if (result.user) {
result.user = Discourse.User.create(result.user);
} }
}); return result;
}
});
window.Discourse.Invite.reopenClass({
create: function(invite) {
var result;
result = this._super(invite);
if (result.user) {
result.user = Discourse.User.create(result.user);
}
return result;
}
});
}).call(this);

View File

@ -1,36 +1,44 @@
(function() { /**
A data model representing a list of Invites
window.Discourse.InviteList = Discourse.Model.extend({ @class InviteList
empty: (function() { @extends Discourse.Model
return this.blank('pending') && this.blank('redeemed'); @namespace Discourse
}).property('pending.@each', 'redeemed.@each') @module Discourse
}); **/
Discourse.InviteList = Discourse.Model.extend({
empty: (function() {
return this.blank('pending') && this.blank('redeemed');
}).property('pending.@each', 'redeemed.@each')
});
window.Discourse.InviteList.reopenClass({ Discourse.InviteList.reopenClass({
findInvitedBy: function(user) {
var promise; findInvitedBy: function(user) {
promise = new RSVP.Promise(); var promise;
jQuery.ajax({ promise = new RSVP.Promise();
url: "/users/" + (user.get('username_lower')) + "/invited.json", jQuery.ajax({
success: function(result) { url: "/users/" + (user.get('username_lower')) + "/invited.json",
var invitedList; success: function(result) {
invitedList = result.invited_list; var invitedList;
if (invitedList.pending) { invitedList = result.invited_list;
invitedList.pending = invitedList.pending.map(function(i) { if (invitedList.pending) {
return Discourse.Invite.create(i); invitedList.pending = invitedList.pending.map(function(i) {
}); return Discourse.Invite.create(i);
} });
if (invitedList.redeemed) {
invitedList.redeemed = invitedList.redeemed.map(function(i) {
return Discourse.Invite.create(i);
});
}
invitedList.user = user;
return promise.resolve(Discourse.InviteList.create(invitedList));
} }
}); if (invitedList.redeemed) {
return promise; invitedList.redeemed = invitedList.redeemed.map(function(i) {
} return Discourse.Invite.create(i);
}); });
}
invitedList.user = user;
return promise.resolve(Discourse.InviteList.create(invitedList));
}
});
return promise;
}
});
}).call(this);

View File

@ -1,54 +0,0 @@
(function() {
Discourse.Mention = (function() {
var cache, load, localCache, lookup, lookupCache;
localCache = {};
cache = function(name, valid) {
localCache[name] = valid;
};
lookupCache = function(name) {
return localCache[name];
};
lookup = function(name, callback) {
var cached;
cached = lookupCache(name);
if (cached === true || cached === false) {
callback(cached);
return false;
} else {
jQuery.get("/users/is_local_username", {
username: name
}, function(r) {
cache(name, r.valid);
return callback(r.valid);
});
return true;
}
};
load = function(e) {
var $elem, loading, username;
$elem = jQuery(e);
if ($elem.data('mention-tested')) {
return;
}
username = $elem.text();
username = username.substr(1);
loading = lookup(username, function(valid) {
if (valid) {
return $elem.replaceWith("<a href='/users/" + (username.toLowerCase()) + "' class='mention'>@" + username + "</a>");
} else {
return $elem.removeClass('mention-loading').addClass('mention-tested');
}
});
if (loading) {
return $elem.addClass('mention-loading');
}
};
return {
load: load,
lookup: lookup,
lookupCache: lookupCache
};
})();
}).call(this);

View File

@ -1,80 +1,78 @@
(function() { /**
A base object we can use to handle models in the Discourse client application.
@class Model
@extends Ember.Object
@uses Discourse.Presence
@namespace Discourse
@module Discourse
**/
Discourse.Model = Ember.Object.extend(Discourse.Presence, {
/** /**
A base object we can use to handle models in the Discourse client application. Our own AJAX handler that handles erronous responses
@class Model @method ajax
@extends Ember.Object @param {String} url The url to contact
@uses Discourse.Presence @param {Object} args The arguments to pass to jQuery.ajax
@namespace Discourse
@module Discourse
**/ **/
window.Discourse.Model = Ember.Object.extend(Discourse.Presence, { ajax: function(url, args) {
var oldError = args.error;
args.error = function(xhr) {
return oldError(jQuery.parseJSON(xhr.responseText).errors);
};
return jQuery.ajax(url, args);
},
/** /**
Our own AJAX handler that handles erronous responses Update our object from another object
@method ajax @method mergeAttributes
@param {String} url The url to contact @param {Object} attrs The attributes we want to merge with
@param {Object} args The arguments to pass to jQuery.ajax @param {Object} builders Optional builders to use when merging attributes
**/ **/
ajax: function(url, args) { mergeAttributes: function(attrs, builders) {
var oldError = args.error; var _this = this;
args.error = function(xhr) { return Object.keys(attrs, function(k, v) {
return oldError(jQuery.parseJSON(xhr.responseText).errors); // If they're in a builder we use that
}; var builder, col;
return jQuery.ajax(url, args); if (typeof v === 'object' && builders && (builder = builders[k])) {
}, if (!_this.get(k)) {
_this.set(k, Em.A());
/**
Update our object from another object
@method mergeAttributes
@param {Object} attrs The attributes we want to merge with
@param {Object} builders Optional builders to use when merging attributes
**/
mergeAttributes: function(attrs, builders) {
var _this = this;
return Object.keys(attrs, function(k, v) {
// If they're in a builder we use that
var builder, col;
if (typeof v === 'object' && builders && (builder = builders[k])) {
if (!_this.get(k)) {
_this.set(k, Em.A());
}
col = _this.get(k);
return v.each(function(obj) {
col.pushObject(builder.create(obj));
});
} else {
_this.set(k, v);
} }
}); col = _this.get(k);
} return v.each(function(obj) {
}); col.pushObject(builder.create(obj));
});
window.Discourse.Model.reopenClass({ } else {
_this.set(k, v);
/**
Given an array of values, return them in a hash
@method extractByKey
@param {Object} collection The collection of values
@param {Object} klass Optional The class to instantiate
**/
extractByKey: function(collection, klass) {
var retval;
retval = {};
if (!collection) {
return retval;
} }
collection.each(function(c) { });
var obj; }
obj = klass.create(c); });
retval[c.id] = obj;
}); Discourse.Model.reopenClass({
/**
Given an array of values, return them in a hash
@method extractByKey
@param {Object} collection The collection of values
@param {Object} klass Optional The class to instantiate
**/
extractByKey: function(collection, klass) {
var retval;
retval = {};
if (!collection) {
return retval; return retval;
} }
}); collection.each(function(c) {
var obj;
obj = klass.create(c);
retval[c.id] = obj;
});
return retval;
}
});
}).call(this);

View File

@ -1,72 +1,68 @@
/**
A data model representing a navigation item on the list views
/* closure wrapping means this does not leak into global context @class InviteList
*/ @extends Discourse.Model
@namespace Discourse
@module Discourse
**/
var validAnon, validNavNames;
validNavNames = ['read', 'popular', 'categories', 'favorited', 'category', 'unread', 'new', 'posted'];
validAnon = ['popular', 'category', 'categories'];
Discourse.NavItem = Discourse.Model.extend({
(function() { categoryName: (function() {
var validAnon, validNavNames; var split;
split = this.get('name').split('/');
validNavNames = ['read', 'popular', 'categories', 'favorited', 'category', 'unread', 'new', 'posted']; if (split[0] === 'category') {
return split[1];
validAnon = ['popular', 'category', 'categories']; } else {
return null;
window.Discourse.NavItem = Discourse.Model.extend({ }
categoryName: (function() { }).property(),
var split; href: (function() {
split = this.get('name').split('/'); /* href from this item
if (split[0] === 'category') {
return split[1];
} else {
return null;
}
}).property(),
href: (function() {
/* href from this item
*/
var name;
name = this.get('name');
if (name === 'category') {
return "/" + name + "/" + (this.get('categoryName'));
} else {
return "/" + name;
}
}).property()
});
Discourse.NavItem.reopenClass({
/* create a nav item from the text, will return null if there is not valid nav item for this particular text
*/ */
fromText: function(text, opts) { var name;
var countSummary, hasCategories, loggedOn, name, split, testName; name = this.get('name');
countSummary = opts.countSummary; if (name === 'category') {
loggedOn = opts.loggedOn; return "/" + name + "/" + (this.get('categoryName'));
hasCategories = opts.hasCategories; } else {
split = text.split(","); return "/" + name;
name = split[0];
testName = name.split("/")[0];
if (!loggedOn && !validAnon.contains(testName)) {
return null;
}
if (!hasCategories && testName === "categories") {
return null;
}
if (!validNavNames.contains(testName)) {
return null;
}
opts = {
name: name,
hasIcon: name === "unread" || name === "favorited",
filters: split.splice(1)
};
if (countSummary) {
if (countSummary && countSummary[name]) {
opts.count = countSummary[name];
}
}
return Discourse.NavItem.create(opts);
} }
}); }).property()
});
Discourse.NavItem.reopenClass({
// create a nav item from the text, will return null if there is not valid nav item for this particular text
fromText: function(text, opts) {
var countSummary, hasCategories, loggedOn, name, split, testName;
countSummary = opts.countSummary;
loggedOn = opts.loggedOn;
hasCategories = opts.hasCategories;
split = text.split(",");
name = split[0];
testName = name.split("/")[0];
if (!loggedOn && !validAnon.contains(testName)) return null;
if (!hasCategories && testName === "categories") return null;
if (!validNavNames.contains(testName)) return null;
opts = {
name: name,
hasIcon: name === "unread" || name === "favorited",
filters: split.splice(1)
};
if (countSummary) {
if (countSummary && countSummary[name]) {
opts.count = countSummary[name];
}
}
return Discourse.NavItem.create(opts);
}
});
}).call(this);

View File

@ -1,40 +1,45 @@
(function() { /**
A data model representing a notification a user receives
window.Discourse.Notification = Discourse.Model.extend({ @class Notification
readClass: (function() { @extends Discourse.Model
if (this.read) { @namespace Discourse
return 'read'; @module Discourse
} else { **/
return ''; Discourse.Notification = Discourse.Model.extend({
}
}).property('read'),
url: (function() {
var slug;
if (this.blank('data.topic_title')) {
return "";
}
slug = this.get('slug');
return "/t/" + slug + "/" + (this.get('topic_id')) + "/" + (this.get('post_number'));
}).property(),
rendered: (function() {
var notificationName;
notificationName = Discourse.get('site.notificationLookup')[this.notification_type];
return Em.String.i18n("notifications." + notificationName, {
username: this.data.display_username,
link: "<a href='" + (this.get('url')) + "'>" + this.data.topic_title + "</a>"
});
}).property()
});
window.Discourse.Notification.reopenClass({ readClass: (function() {
create: function(obj) { if (this.read) return 'read';
var result; return '';
result = this._super(obj); }).property('read'),
if (obj.data) {
result.set('data', Em.Object.create(obj.data)); url: (function() {
} var slug;
return result; if (this.blank('data.topic_title')) return "";
slug = this.get('slug');
return "/t/" + slug + "/" + (this.get('topic_id')) + "/" + (this.get('post_number'));
}).property(),
rendered: (function() {
var notificationName;
notificationName = Discourse.get('site.notificationLookup')[this.notification_type];
return Em.String.i18n("notifications." + notificationName, {
username: this.data.display_username,
link: "<a href='" + (this.get('url')) + "'>" + this.data.topic_title + "</a>"
});
}).property()
});
Discourse.Notification.reopenClass({
create: function(obj) {
var result;
result = this._super(obj);
if (obj.data) {
result.set('data', Em.Object.create(obj.data));
} }
}); return result;
}
});
}).call(this);

View File

@ -1,83 +0,0 @@
(function() {
Discourse.Onebox = (function() {
/* for now it only stores in a var, in future we can change it so it uses localStorage,
*/
/* trouble with localStorage is that expire semantics need some thinking
*/
/*cacheKey = "__onebox__"
*/
var cache, load, localCache, lookup, lookupCache;
localCache = {};
cache = function(url, contents) {
localCache[url] = contents;
return null;
};
lookupCache = function(url) {
var cached;
cached = localCache[url];
if (cached && cached.then) {
return null;
} else {
return cached;
}
};
lookup = function(url, refresh, callback) {
var cached;
cached = localCache[url];
if (refresh && cached && !cached.then) {
cached = null;
}
if (cached) {
if (cached.then) {
cached.then(callback(lookupCache(url)));
} else {
callback(cached);
}
return false;
} else {
cache(url, jQuery.get("/onebox", {
url: url,
refresh: refresh
}, function(html) {
cache(url, html);
return callback(html);
}));
return true;
}
};
load = function(e, refresh) {
var $elem, loading, url;
if (!refresh) refresh = false;
url = e.href;
$elem = jQuery(e);
if ($elem.data('onebox-loaded')) {
return;
}
loading = lookup(url, refresh, function(html) {
$elem.removeClass('loading-onebox');
$elem.data('onebox-loaded');
if (!html) {
return;
}
if (html.trim().length === 0) {
return;
}
return $elem.replaceWith(html);
});
if (loading) {
return $elem.addClass('loading-onebox');
}
};
return {
load: load,
lookup: lookup,
lookupCache: lookupCache
};
})();
}).call(this);

View File

@ -1,370 +1,341 @@
(function() { /**
A data model representing a post in a topic
window.Discourse.Post = Discourse.Model.extend({ @class Post
/* Url to this post @extends Discourse.Model
*/ @namespace Discourse
@module Discourse
**/
Discourse.Post = Discourse.Model.extend({
url: (function() { url: (function() {
return Discourse.Utilities.postUrl(this.get('topic.slug') || this.get('topic_slug'), this.get('topic_id'), this.get('post_number')); return Discourse.Utilities.postUrl(this.get('topic.slug') || this.get('topic_slug'), this.get('topic_id'), this.get('post_number'));
}).property('post_number', 'topic_id', 'topic.slug'), }).property('post_number', 'topic_id', 'topic.slug'),
originalPostUrl: (function() {
return "/t/" + (this.get('topic_id')) + "/" + (this.get('reply_to_post_number')); originalPostUrl: (function() {
}).property('reply_to_post_number'), return "/t/" + (this.get('topic_id')) + "/" + (this.get('reply_to_post_number'));
showUserReplyTab: (function() { }).property('reply_to_post_number'),
return this.get('reply_to_user') && (this.get('reply_to_post_number') < (this.get('post_number') - 1));
}).property('reply_to_user', 'reply_to_post_number', 'post_number'), showUserReplyTab: (function() {
firstPost: (function() { return this.get('reply_to_user') && (this.get('reply_to_post_number') < (this.get('post_number') - 1));
if (this.get('bestOfFirst') === true) { }).property('reply_to_user', 'reply_to_post_number', 'post_number'),
firstPost: (function() {
if (this.get('bestOfFirst') === true) return true;
return this.get('post_number') === 1;
}).property('post_number'),
hasHistory: (function() {
return this.get('version') > 1;
}).property('version'),
postElementId: (function() {
return "post_" + (this.get('post_number'));
}).property(),
// The class for the read icon of the post. It starts with read-icon then adds 'seen' or
// 'last-read' if the post has been seen or is the highest post number seen so far respectively.
bookmarkClass: (function() {
var result, topic;
result = 'read-icon';
if (this.get('bookmarked')) return result + ' bookmarked';
topic = this.get('topic');
if (topic && topic.get('last_read_post_number') === this.get('post_number')) {
result += ' last-read';
} else {
if (this.get('read')) {
result += ' seen';
}
}
return result;
}).property('read', 'topic.last_read_post_number', 'bookmarked'),
// Custom tooltips for the bookmark icons
bookmarkTooltip: (function() {
var topic;
if (this.get('bookmarked')) return Em.String.i18n('bookmarks.created');
if (!this.get('read')) return "";
topic = this.get('topic');
if (topic && topic.get('last_read_post_number') === this.get('post_number')) {
return Em.String.i18n('bookmarks.last_read');
}
return Em.String.i18n('bookmarks.not_bookmarked');
}).property('read', 'topic.last_read_post_number', 'bookmarked'),
bookmarkedChanged: (function() {
var _this = this;
return jQuery.ajax({
url: "/posts/" + (this.get('id')) + "/bookmark",
type: 'PUT',
data: {
bookmarked: this.get('bookmarked') ? true : false
},
error: function(error) {
var errors;
errors = jQuery.parseJSON(error.responseText).errors;
bootbox.alert(errors[0]);
return _this.toggleProperty('bookmarked');
}
});
}).observes('bookmarked'),
internalLinks: (function() {
if (this.blank('link_counts')) return null;
return this.get('link_counts').filterProperty('internal').filterProperty('title');
}).property('link_counts.@each.internal'),
// Edits are the version - 1, so version 2 = 1 edit
editCount: (function() {
return this.get('version') - 1;
}).property('version'),
historyHeat: (function() {
var rightNow, updatedAt, updatedAtDate;
if (!(updatedAt = this.get('updated_at'))) return;
rightNow = new Date().getTime();
// Show heat on age
updatedAtDate = Date.create(updatedAt).getTime();
if (updatedAtDate > (rightNow - 60 * 60 * 1000 * 12)) return 'heatmap-high';
if (updatedAtDate > (rightNow - 60 * 60 * 1000 * 24)) return 'heatmap-med';
if (updatedAtDate > (rightNow - 60 * 60 * 1000 * 48)) return 'heatmap-low';
}).property('updated_at'),
flagsAvailable: (function() {
var _this = this;
return Discourse.get('site.flagTypes').filter(function(item) {
return _this.get("actionByName." + (item.get('name_key')) + ".can_act");
});
}).property('Discourse.site.flagTypes', 'actions_summary.@each.can_act'),
actionsHistory: (function() {
if (!this.present('actions_summary')) return null;
return this.get('actions_summary').filter(function(i) {
if (i.get('count') === 0) {
return false;
}
if (i.get('users') && i.get('users').length > 0) {
return true; return true;
} }
return this.get('post_number') === 1; return !i.get('hidden');
}).property('post_number'), });
hasHistory: (function() { }).property('actions_summary.@each.users', 'actions_summary.@each.count'),
return this.get('version') > 1;
}).property('version'),
postElementId: (function() {
return "post_" + (this.get('post_number'));
}).property(),
/*
The class for the read icon of the post. It starts with read-icon then adds 'seen' or
'last-read' if the post has been seen or is the highest post number seen so far respectively.
*/
bookmarkClass: (function() { // Save a post and call the callback when done.
var result, topic; save: function(complete, error) {
result = 'read-icon'; var data, metaData;
if (this.get('bookmarked')) { if (!this.get('newPost')) {
return result + ' bookmarked'; // We're updating a post
}
topic = this.get('topic');
if (topic && topic.get('last_read_post_number') === this.get('post_number')) {
result += ' last-read';
} else {
if (this.get('read')) {
result += ' seen';
}
}
return result;
}).property('read', 'topic.last_read_post_number', 'bookmarked'),
/* Custom tooltips for the bookmark icons
*/
bookmarkTooltip: (function() {
var topic;
if (this.get('bookmarked')) {
return Em.String.i18n('bookmarks.created');
}
if (!this.get('read')) {
return "";
}
topic = this.get('topic');
if (topic && topic.get('last_read_post_number') === this.get('post_number')) {
return Em.String.i18n('bookmarks.last_read');
}
return Em.String.i18n('bookmarks.not_bookmarked');
}).property('read', 'topic.last_read_post_number', 'bookmarked'),
bookmarkedChanged: (function() {
var _this = this;
return jQuery.ajax({ return jQuery.ajax({
url: "/posts/" + (this.get('id')) + "/bookmark", url: "/posts/" + (this.get('id')),
type: 'PUT', type: 'PUT',
data: { data: {
bookmarked: this.get('bookmarked') ? true : false post: { raw: this.get('raw') },
image_sizes: this.get('imageSizes')
}, },
error: function(error) {
var errors;
errors = jQuery.parseJSON(error.responseText).errors;
bootbox.alert(errors[0]);
return _this.toggleProperty('bookmarked');
}
});
}).observes('bookmarked'),
internalLinks: (function() {
if (this.blank('link_counts')) {
return null;
}
return this.get('link_counts').filterProperty('internal').filterProperty('title');
}).property('link_counts.@each.internal'),
/* Edits are the version - 1, so version 2 = 1 edit
*/
editCount: (function() {
return this.get('version') - 1;
}).property('version'),
historyHeat: (function() {
var rightNow, updatedAt, updatedAtDate;
if (!(updatedAt = this.get('updated_at'))) {
return;
}
rightNow = new Date().getTime();
/* Show heat on age
*/
updatedAtDate = Date.create(updatedAt).getTime();
if (updatedAtDate > (rightNow - 60 * 60 * 1000 * 12)) {
return 'heatmap-high';
}
if (updatedAtDate > (rightNow - 60 * 60 * 1000 * 24)) {
return 'heatmap-med';
}
if (updatedAtDate > (rightNow - 60 * 60 * 1000 * 48)) {
return 'heatmap-low';
}
}).property('updated_at'),
flagsAvailable: (function() {
var _this = this;
return Discourse.get('site.flagTypes').filter(function(item) {
return _this.get("actionByName." + (item.get('name_key')) + ".can_act");
});
}).property('Discourse.site.flagTypes', 'actions_summary.@each.can_act'),
actionsHistory: (function() {
if (!this.present('actions_summary')) {
return null;
}
return this.get('actions_summary').filter(function(i) {
if (i.get('count') === 0) {
return false;
}
if (i.get('users') && i.get('users').length > 0) {
return true;
}
return !i.get('hidden');
});
}).property('actions_summary.@each.users', 'actions_summary.@each.count'),
/* Save a post and call the callback when done.
*/
save: function(complete, error) {
var data, metaData;
if (!this.get('newPost')) {
/* We're updating a post
*/
return jQuery.ajax({
url: "/posts/" + (this.get('id')),
type: 'PUT',
data: {
post: { raw: this.get('raw') },
image_sizes: this.get('imageSizes')
},
success: function(result) {
console.log(result)
// If we received a category update, update it
if (result.category) Discourse.get('site').updateCategory(result.category);
return typeof complete === "function" ? complete(Discourse.Post.create(result.post)) : void 0;
},
error: function(result) {
return typeof error === "function" ? error(result) : void 0;
}
});
} else {
// We're saving a post
data = {
post: this.getProperties('raw', 'topic_id', 'reply_to_post_number', 'category'),
archetype: this.get('archetype'),
title: this.get('title'),
image_sizes: this.get('imageSizes'),
target_usernames: this.get('target_usernames')
};
/* Put the metaData into the request
*/
if (metaData = this.get('metaData')) {
data.meta_data = {};
Ember.keys(metaData).forEach(function(key) {
data.meta_data[key] = metaData.get(key);
});
}
return jQuery.ajax({
type: 'POST',
url: "/posts",
data: data,
success: function(result) {
return typeof complete === "function" ? complete(Discourse.Post.create(result)) : void 0;
},
error: function(result) {
return typeof error === "function" ? error(result) : void 0;
}
});
}
},
recover: function() {
return jQuery.ajax("/posts/" + (this.get('id')) + "/recover", {
type: 'PUT',
cache: false
});
},
"delete": function(complete) {
return jQuery.ajax("/posts/" + (this.get('id')), {
type: 'DELETE',
success: function(result) { success: function(result) {
return typeof complete === "function" ? complete() : void 0; // If we received a category update, update it
if (result.category) Discourse.get('site').updateCategory(result.category);
return typeof complete === "function" ? complete(Discourse.Post.create(result.post)) : void 0;
},
error: function(result) { return typeof error === "function" ? error(result) : void 0; }
});
} else {
// We're saving a post
data = {
post: this.getProperties('raw', 'topic_id', 'reply_to_post_number', 'category'),
archetype: this.get('archetype'),
title: this.get('title'),
image_sizes: this.get('imageSizes'),
target_usernames: this.get('target_usernames')
};
// Put the metaData into the request
if (metaData = this.get('metaData')) {
data.meta_data = {};
Ember.keys(metaData).forEach(function(key) { data.meta_data[key] = metaData.get(key); });
}
return jQuery.ajax({
type: 'POST',
url: "/posts",
data: data,
success: function(result) {
return typeof complete === "function" ? complete(Discourse.Post.create(result)) : void 0;
},
error: function(result) {
return typeof error === "function" ? error(result) : void 0;
} }
}); });
},
/*
Update the properties of this post from an obj, ignoring cooked as we should already
have that rendered.
*/
updateFromSave: function(obj) {
var lookup,
_this = this;
if (!obj) {
return;
}
Object.each(obj, function(key, val) {
if (key === 'actions_summary') {
return false;
}
if (val) {
return _this.set(key, val);
}
});
/* Rebuild actions summary
*/
this.set('actions_summary', Em.A());
if (obj.actions_summary) {
lookup = Em.Object.create();
obj.actions_summary.each(function(a) {
var actionSummary;
a.post = _this;
a.actionType = Discourse.get("site").postActionTypeById(a.id);
actionSummary = Discourse.ActionSummary.create(a);
_this.get('actions_summary').pushObject(actionSummary);
return lookup.set(a.actionType.get('name_key'), actionSummary);
});
return this.set('actionByName', lookup);
}
},
// Load replies to this post
loadReplies: function() {
var promise,
_this = this;
promise = new RSVP.Promise();
this.set('loadingReplies', true);
this.set('replies', []);
jQuery.getJSON("/posts/" + (this.get('id')) + "/replies", function(loaded) {
loaded.each(function(reply) {
var post;
post = Discourse.Post.create(reply);
post.set('topic', _this.get('topic'));
return _this.get('replies').pushObject(post);
});
_this.set('loadingReplies', false);
return promise.resolve();
});
return promise;
},
loadVersions: function(callback) {
return jQuery.get("/posts/" + (this.get('id')) + "/versions.json", function(result) {
return callback(result);
});
},
// Whether to show replies directly below
showRepliesBelow: (function() {
var reply_count, _ref;
reply_count = this.get('reply_count');
/* We don't show replies if there aren't any
*/
if (reply_count === 0) {
return false;
}
/* Always show replies if the setting `supress_reply_directly_below` is false.
*/
if (!Discourse.SiteSettings.supress_reply_directly_below) {
return true;
}
/*Always show replies if there's more than one
*/
if (reply_count > 1) {
return true;
}
/* If we have *exactly* one reply, we have to consider if it's directly below us
*/
if ((_ref = this.get('topic')) ? _ref.isReplyDirectlyBelow(this) : void 0) {
return false;
}
return true;
}).property('reply_count')
});
window.Discourse.Post.reopenClass({
createActionSummary: function(result) {
var lookup;
if (result.actions_summary) {
lookup = Em.Object.create();
result.actions_summary = result.actions_summary.map(function(a) {
var actionSummary;
a.post = result;
a.actionType = Discourse.get("site").postActionTypeById(a.id);
actionSummary = Discourse.ActionSummary.create(a);
lookup.set(a.actionType.get('name_key'), actionSummary);
return actionSummary;
});
return result.set('actionByName', lookup);
}
},
create: function(obj, topic) {
var result;
result = this._super(obj);
this.createActionSummary(result);
if (obj.reply_to_user) {
result.set('reply_to_user', Discourse.User.create(obj.reply_to_user));
}
result.set('topic', topic);
return result;
},
deleteMany: function(posts) {
return jQuery.ajax("/posts/destroy_many", {
type: 'DELETE',
data: {
post_ids: posts.map(function(p) {
return p.get('id');
})
}
});
},
loadVersion: function(postId, version, callback) {
var _this = this;
return jQuery.getJSON("/posts/" + postId + ".json?version=" + version, function(result) {
return callback(Discourse.Post.create(result));
});
},
loadByPostNumber: function(topicId, postId, callback) {
var _this = this;
return jQuery.getJSON("/posts/by_number/" + topicId + "/" + postId + ".json", function(result) {
return callback(Discourse.Post.create(result));
});
},
loadQuote: function(postId) {
var promise,
_this = this;
promise = new RSVP.Promise();
jQuery.getJSON("/posts/" + postId + ".json", function(result) {
var post;
post = Discourse.Post.create(result);
return promise.resolve(Discourse.BBCode.buildQuoteBBCode(post, post.get('raw')));
});
return promise;
},
load: function(postId, callback) {
var _this = this;
return jQuery.getJSON("/posts/" + postId + ".json", function(result) {
return callback(Discourse.Post.create(result));
});
} }
}); },
recover: function() {
return jQuery.ajax("/posts/" + (this.get('id')) + "/recover", { type: 'PUT', cache: false });
},
"delete": function(complete) {
return jQuery.ajax("/posts/" + (this.get('id')), {
type: 'DELETE',
success: function(result) {
return typeof complete === "function" ? complete() : void 0;
}
});
},
// Update the properties of this post from an obj, ignoring cooked as we should already
// have that rendered.
updateFromSave: function(obj) {
var lookup,
_this = this;
if (!obj) {
return;
}
Object.each(obj, function(key, val) {
if (key === 'actions_summary') {
return false;
}
if (val) {
return _this.set(key, val);
}
});
// Rebuild actions summary
this.set('actions_summary', Em.A());
if (obj.actions_summary) {
lookup = Em.Object.create();
obj.actions_summary.each(function(a) {
var actionSummary;
a.post = _this;
a.actionType = Discourse.get("site").postActionTypeById(a.id);
actionSummary = Discourse.ActionSummary.create(a);
_this.get('actions_summary').pushObject(actionSummary);
return lookup.set(a.actionType.get('name_key'), actionSummary);
});
return this.set('actionByName', lookup);
}
},
// Load replies to this post
loadReplies: function() {
var promise,
_this = this;
promise = new RSVP.Promise();
this.set('loadingReplies', true);
this.set('replies', []);
jQuery.getJSON("/posts/" + (this.get('id')) + "/replies", function(loaded) {
loaded.each(function(reply) {
var post;
post = Discourse.Post.create(reply);
post.set('topic', _this.get('topic'));
return _this.get('replies').pushObject(post);
});
_this.set('loadingReplies', false);
return promise.resolve();
});
return promise;
},
loadVersions: function(callback) {
return jQuery.get("/posts/" + (this.get('id')) + "/versions.json", function(result) {
return callback(result);
});
},
// Whether to show replies directly below
showRepliesBelow: (function() {
var reply_count, _ref;
reply_count = this.get('reply_count');
// We don't show replies if there aren't any
if (reply_count === 0) return false;
// Always show replies if the setting `supress_reply_directly_below` is false.
if (!Discourse.SiteSettings.supress_reply_directly_below) return true;
// Always show replies if there's more than one
if (reply_count > 1) return true;
// If we have *exactly* one reply, we have to consider if it's directly below us
if ((_ref = this.get('topic')) ? _ref.isReplyDirectlyBelow(this) : void 0) return false;
return true;
}).property('reply_count')
});
window.Discourse.Post.reopenClass({
createActionSummary: function(result) {
var lookup;
if (result.actions_summary) {
lookup = Em.Object.create();
result.actions_summary = result.actions_summary.map(function(a) {
var actionSummary;
a.post = result;
a.actionType = Discourse.get("site").postActionTypeById(a.id);
actionSummary = Discourse.ActionSummary.create(a);
lookup.set(a.actionType.get('name_key'), actionSummary);
return actionSummary;
});
return result.set('actionByName', lookup);
}
},
create: function(obj, topic) {
var result;
result = this._super(obj);
this.createActionSummary(result);
if (obj.reply_to_user) {
result.set('reply_to_user', Discourse.User.create(obj.reply_to_user));
}
result.set('topic', topic);
return result;
},
deleteMany: function(posts) {
return jQuery.ajax("/posts/destroy_many", {
type: 'DELETE',
data: {
post_ids: posts.map(function(p) {
return p.get('id');
})
}
});
},
loadVersion: function(postId, version, callback) {
var _this = this;
return jQuery.getJSON("/posts/" + postId + ".json?version=" + version, function(result) {
return callback(Discourse.Post.create(result));
});
},
loadByPostNumber: function(topicId, postId, callback) {
var _this = this;
return jQuery.getJSON("/posts/by_number/" + topicId + "/" + postId + ".json", function(result) {
return callback(Discourse.Post.create(result));
});
},
loadQuote: function(postId) {
var promise,
_this = this;
promise = new RSVP.Promise();
jQuery.getJSON("/posts/" + postId + ".json", function(result) {
var post;
post = Discourse.Post.create(result);
return promise.resolve(Discourse.BBCode.buildQuoteBBCode(post, post.get('raw')));
});
return promise;
},
load: function(postId, callback) {
var _this = this;
return jQuery.getJSON("/posts/" + postId + ".json", function(result) {
return callback(Discourse.Post.create(result));
});
}
});
}).call(this);

View File

@ -1,16 +1,23 @@
(function() { /**
A data model representing action types (flags, likes) against a Post
@class PostActionType
@extends Discourse.Model
@namespace Discourse
@module Discourse
**/
Discourse.PostActionType = Discourse.Model.extend({
alsoName: (function() {
if (this.get('is_flag')) return Em.String.i18n('post.actions.flag');
return this.get('name');
}).property('is_flag', 'name'),
alsoNameLower: (function() {
var _ref;
return (_ref = this.get('alsoName')) ? _ref.toLowerCase() : void 0;
}).property('alsoName')
});
window.Discourse.PostActionType = Discourse.Model.extend({
alsoName: (function() {
if (this.get('is_flag')) {
return Em.String.i18n('post.actions.flag');
}
return this.get('name');
}).property('is_flag', 'name'),
alsoNameLower: (function() {
var _ref;
return (_ref = this.get('alsoName')) ? _ref.toLowerCase() : void 0;
}).property('alsoName')
});
}).call(this);

View File

@ -1,60 +1,66 @@
(function() { /**
A data model representing the site (instance of Discourse)
window.Discourse.Site = Discourse.Model.extend({ @class Site
@extends Discourse.Model
@namespace Discourse
@module Discourse
**/
Discourse.Site = Discourse.Model.extend({
notificationLookup: (function() { notificationLookup: (function() {
var result; var result;
result = []; result = [];
Object.keys(this.get('notification_types'), function(k, v) { Object.keys(this.get('notification_types'), function(k, v) {
result[v] = k; result[v] = k;
}); });
return result; return result;
}).property('notification_types'), }).property('notification_types'),
flagTypes: (function() { flagTypes: (function() {
var postActionTypes; var postActionTypes;
postActionTypes = this.get('post_action_types'); postActionTypes = this.get('post_action_types');
if (!postActionTypes) { if (!postActionTypes) {
return []; return [];
}
return postActionTypes.filterProperty('is_flag', true);
}).property('post_action_types.@each'),
postActionTypeById: function(id) {
return this.get("postActionByIdLookup.action" + id);
},
updateCategory: function(newCategory) {
var existingCategory = this.get('categories').findProperty('id', Em.get(newCategory, 'id'));
if (existingCategory) existingCategory.mergeAttributes(newCategory);
}
});
Discourse.Site.reopenClass({
create: function(obj) {
var _this = this;
return Object.tap(this._super(obj), function(result) {
if (result.categories) {
result.categories = result.categories.map(function(c) {
return Discourse.Category.create(c);
});
} }
return postActionTypes.filterProperty('is_flag', true); if (result.post_action_types) {
}).property('post_action_types.@each'), result.postActionByIdLookup = Em.Object.create();
result.post_action_types = result.post_action_types.map(function(p) {
var actionType;
actionType = Discourse.PostActionType.create(p);
result.postActionByIdLookup.set("action" + p.id, actionType);
return actionType;
});
}
if (result.archetypes) {
result.archetypes = result.archetypes.map(function(a) {
return Discourse.Archetype.create(a);
});
}
});
}
});
postActionTypeById: function(id) {
return this.get("postActionByIdLookup.action" + id);
},
updateCategory: function(newCategory) {
var existingCategory = this.get('categories').findProperty('id', Em.get(newCategory, 'id'));
if (existingCategory) existingCategory.mergeAttributes(newCategory);
}
});
window.Discourse.Site.reopenClass({
create: function(obj) {
var _this = this;
return Object.tap(this._super(obj), function(result) {
if (result.categories) {
result.categories = result.categories.map(function(c) {
return Discourse.Category.create(c);
});
}
if (result.post_action_types) {
result.postActionByIdLookup = Em.Object.create();
result.post_action_types = result.post_action_types.map(function(p) {
var actionType;
actionType = Discourse.PostActionType.create(p);
result.postActionByIdLookup.set("action" + p.id, actionType);
return actionType;
});
}
if (result.archetypes) {
result.archetypes = result.archetypes.map(function(a) {
return Discourse.Archetype.create(a);
});
}
});
}
});
}).call(this);

View File

@ -1,468 +1,428 @@
(function() { /**
A data model representing a Topic
Discourse.Topic = Discourse.Model.extend({ @class Topic
categoriesBinding: 'Discourse.site.categories', @extends Discourse.Model
fewParticipants: (function() { @namespace Discourse
if (!this.present('participants')) { @module Discourse
return null; **/
} Discourse.Topic = Discourse.Model.extend({
return this.get('participants').slice(0, 3); categoriesBinding: 'Discourse.site.categories',
}).property('participants'),
canConvertToRegular: (function() {
var a;
a = this.get('archetype');
return a !== 'regular' && a !== 'private_message';
}).property('archetype'),
convertArchetype: function(archetype) {
var a;
a = this.get('archetype');
if (a !== 'regular' && a !== 'private_message') {
this.set('archetype', 'regular');
return jQuery.post(this.get('url'), {
_method: 'put',
archetype: 'regular'
});
}
},
category: (function() {
if (this.get('categories')) {
return this.get('categories').findProperty('name', this.get('categoryName'));
}
}).property('categoryName', 'categories'),
url: (function() {
var slug;
slug = this.get('slug');
if (slug.isBlank()) {
slug = "topic";
}
return "/t/" + slug + "/" + (this.get('id'));
}).property('id', 'slug'),
/* Helper to build a Url with a post number
*/
urlForPostNumber: function(postNumber) { fewParticipants: (function() {
var url; if (!this.present('participants')) return null;
url = this.get('url'); return this.get('participants').slice(0, 3);
if (postNumber && (postNumber > 1)) { }).property('participants'),
url += "/" + postNumber;
}
return url;
},
lastReadUrl: (function() {
return this.urlForPostNumber(this.get('last_read_post_number'));
}).property('url', 'last_read_post_number'),
lastPostUrl: (function() {
return this.urlForPostNumber(this.get('highest_post_number'));
}).property('url', 'highest_post_number'),
/* The last post in the topic
*/
lastPost: function() { canConvertToRegular: (function() {
return this.get('posts').last(); var a = this.get('archetype');
}, return a !== 'regular' && a !== 'private_message';
postsChanged: (function() { }).property('archetype'),
var last, posts;
posts = this.get('posts');
last = posts.last();
if (!(last && last.set && !last.lastPost)) {
return;
}
posts.each(function(p) {
if (p.lastPost) {
return p.set('lastPost', false);
}
});
last.set('lastPost', true);
return true;
}).observes('posts.@each', 'posts'),
/* The amount of new posts to display. It might be different than what the server
*/
/* tells us if we are still asynchronously flushing our "recently read" data. convertArchetype: function(archetype) {
*/ var a;
a = this.get('archetype');
/* So take what the browser has seen into consideration. if (a !== 'regular' && a !== 'private_message') {
*/ this.set('archetype', 'regular');
displayNewPosts: (function() {
var delta, highestSeen, result;
if (highestSeen = Discourse.get('highestSeenByTopic')[this.get('id')]) {
delta = highestSeen - this.get('last_read_post_number');
if (delta > 0) {
result = this.get('new_posts') - delta;
if (result < 0) {
result = 0;
}
return result;
}
}
return this.get('new_posts');
}).property('new_posts', 'id'),
/* The coldmap class for the age of the topic
*/
ageCold: (function() {
var createdAt, createdAtDays, daysSinceEpoch, lastPost, nowDays;
if (!(lastPost = this.get('last_posted_at'))) {
return;
}
if (!(createdAt = this.get('created_at'))) {
return;
}
daysSinceEpoch = function(dt) {
/* 1000 * 60 * 60 * 24 = days since epoch
*/
return dt.getTime() / 86400000;
};
/* Show heat on age
*/
nowDays = daysSinceEpoch(new Date());
createdAtDays = daysSinceEpoch(new Date(createdAt));
if (daysSinceEpoch(new Date(lastPost)) > nowDays - 90) {
if (createdAtDays < nowDays - 60) {
return 'coldmap-high';
}
if (createdAtDays < nowDays - 30) {
return 'coldmap-med';
}
if (createdAtDays < nowDays - 14) {
return 'coldmap-low';
}
}
return null;
}).property('age', 'created_at'),
archetypeObject: (function() {
return Discourse.get('site.archetypes').findProperty('id', this.get('archetype'));
}).property('archetype'),
isPrivateMessage: (function() {
return this.get('archetype') === 'private_message';
}).property('archetype'),
/* Does this topic only have a single post?
*/
singlePost: (function() {
return this.get('posts_count') === 1;
}).property('posts_count'),
toggleStatus: function(property) {
this.toggleProperty(property);
return jQuery.post("" + (this.get('url')) + "/status", {
_method: 'put',
status: property,
enabled: this.get(property) ? 'true' : 'false'
});
},
toggleStar: function() {
var _this = this;
this.toggleProperty('starred');
return jQuery.ajax({
url: "" + (this.get('url')) + "/star",
type: 'PUT',
data: {
starred: this.get('starred') ? true : false
},
error: function(error) {
var errors;
_this.toggleProperty('starred');
errors = jQuery.parseJSON(error.responseText).errors;
return bootbox.alert(errors[0]);
}
});
},
/* Save any changes we've made to the model
*/
save: function() {
/* Don't save unless we can
*/
if (!this.get('can_edit')) {
return;
}
return jQuery.post(this.get('url'), { return jQuery.post(this.get('url'), {
_method: 'put', _method: 'put',
title: this.get('title'), archetype: 'regular'
category: this.get('category.name')
}); });
}, }
/* Reset our read data for this topic },
*/
resetRead: function(callback) { category: (function() {
return jQuery.ajax("/t/" + (this.get('id')) + "/timings", { if (this.get('categories')) {
type: 'DELETE', return this.get('categories').findProperty('name', this.get('categoryName'));
success: function() { }
return typeof callback === "function" ? callback() : void 0; }).property('categoryName', 'categories'),
url: (function() {
var slug = this.get('slug');
if (slug.isBlank()) {
slug = "topic";
}
return "/t/" + slug + "/" + (this.get('id'));
}).property('id', 'slug'),
// Helper to build a Url with a post number
urlForPostNumber: function(postNumber) {
var url;
url = this.get('url');
if (postNumber && (postNumber > 1)) {
url += "/" + postNumber;
}
return url;
},
lastReadUrl: (function() {
return this.urlForPostNumber(this.get('last_read_post_number'));
}).property('url', 'last_read_post_number'),
lastPostUrl: (function() {
return this.urlForPostNumber(this.get('highest_post_number'));
}).property('url', 'highest_post_number'),
// The last post in the topic
lastPost: function() {
return this.get('posts').last();
},
postsChanged: (function() {
var last, posts;
posts = this.get('posts');
last = posts.last();
if (!(last && last.set && !last.lastPost)) return;
posts.each(function(p) {
if (p.lastPost) return p.set('lastPost', false);
});
last.set('lastPost', true);
return true;
}).observes('posts.@each', 'posts'),
// The amount of new posts to display. It might be different than what the server
// tells us if we are still asynchronously flushing our "recently read" data.
// So take what the browser has seen into consideration.
displayNewPosts: (function() {
var delta, highestSeen, result;
if (highestSeen = Discourse.get('highestSeenByTopic')[this.get('id')]) {
delta = highestSeen - this.get('last_read_post_number');
if (delta > 0) {
result = this.get('new_posts') - delta;
if (result < 0) {
result = 0;
} }
}); return result;
},
/* Invite a user to this topic
*/
inviteUser: function(user) {
return jQuery.ajax({
type: 'POST',
url: "/t/" + (this.get('id')) + "/invite",
data: {
user: user
}
});
},
/* Delete this topic
*/
"delete": function(callback) {
return jQuery.ajax("/t/" + (this.get('id')), {
type: 'DELETE',
success: function() {
return typeof callback === "function" ? callback() : void 0;
}
});
},
/* Load the posts for this topic
*/
loadPosts: function(opts) {
var _this = this;
if (!opts) {
opts = {};
} }
/* Load the first post by default }
*/ return this.get('new_posts');
}).property('new_posts', 'id'),
if (!opts.bestOf) { // The coldmap class for the age of the topic
if (!opts.nearPost) opts.nearPost = 1 ageCold: (function() {
var createdAt, createdAtDays, daysSinceEpoch, lastPost, nowDays;
if (!(lastPost = this.get('last_posted_at'))) return;
if (!(createdAt = this.get('created_at'))) return;
daysSinceEpoch = function(dt) {
// 1000 * 60 * 60 * 24 = days since epoch
return dt.getTime() / 86400000;
};
// Show heat on age
nowDays = daysSinceEpoch(new Date());
createdAtDays = daysSinceEpoch(new Date(createdAt));
if (daysSinceEpoch(new Date(lastPost)) > nowDays - 90) {
if (createdAtDays < nowDays - 60) return 'coldmap-high';
if (createdAtDays < nowDays - 30) return 'coldmap-med';
if (createdAtDays < nowDays - 14) return 'coldmap-low';
}
return null;
}).property('age', 'created_at'),
archetypeObject: (function() {
return Discourse.get('site.archetypes').findProperty('id', this.get('archetype'));
}).property('archetype'),
isPrivateMessage: (function() {
return this.get('archetype') === 'private_message';
}).property('archetype'),
// Does this topic only have a single post?
singlePost: (function() {
return this.get('posts_count') === 1;
}).property('posts_count'),
toggleStatus: function(property) {
this.toggleProperty(property);
return jQuery.post("" + (this.get('url')) + "/status", {
_method: 'put',
status: property,
enabled: this.get(property) ? 'true' : 'false'
});
},
toggleStar: function() {
var _this = this;
this.toggleProperty('starred');
return jQuery.ajax({
url: "" + (this.get('url')) + "/star",
type: 'PUT',
data: {
starred: this.get('starred') ? true : false
},
error: function(error) {
var errors;
_this.toggleProperty('starred');
errors = jQuery.parseJSON(error.responseText).errors;
return bootbox.alert(errors[0]);
} }
/* If we already have that post in the DOM, jump to it });
*/ },
if (Discourse.TopicView.scrollTo(this.get('id'), opts.nearPost)) { // Save any changes we've made to the model
return; save: function() {
// Don't save unless we can
if (!this.get('can_edit')) return;
return jQuery.post(this.get('url'), {
_method: 'put',
title: this.get('title'),
category: this.get('category.name')
});
},
// Reset our read data for this topic
resetRead: function(callback) {
return jQuery.ajax("/t/" + (this.get('id')) + "/timings", {
type: 'DELETE',
success: function() {
return typeof callback === "function" ? callback() : void 0;
} }
return Discourse.Topic.find(this.get('id'), { });
nearPost: opts.nearPost, },
bestOf: opts.bestOf,
trackVisit: opts.trackVisit
}).then(function(result) {
/* If loading the topic succeeded...
*/
/* Update the slug if different // Invite a user to this topic
*/ inviteUser: function(user) {
return jQuery.ajax({
type: 'POST',
url: "/t/" + (this.get('id')) + "/invite",
data: {
user: user
}
});
},
var closestPostNumber, lastPost, postDiff; // Delete this topic
if (result.slug) { "delete": function(callback) {
_this.set('slug', result.slug); return jQuery.ajax("/t/" + (this.get('id')), {
} type: 'DELETE',
/* If we want to scroll to a post that doesn't exist, just pop them to the closest success: function() {
*/ return typeof callback === "function" ? callback() : void 0;
}
});
},
/* one instead. This is likely happening due to a deleted post. // Load the posts for this topic
*/ loadPosts: function(opts) {
var _this = this;
if (!opts) {
opts = {};
}
opts.nearPost = parseInt(opts.nearPost, 10); // Load the first post by default
closestPostNumber = 0; if (!opts.bestOf) {
postDiff = Number.MAX_VALUE; if (!opts.nearPost) opts.nearPost = 1
result.posts.each(function(p) { }
var diff;
diff = Math.abs(p.post_number - opts.nearPost); // If we already have that post in the DOM, jump to it
if (diff < postDiff) { if (Discourse.TopicView.scrollTo(this.get('id'), opts.nearPost)) return;
postDiff = diff;
closestPostNumber = p.post_number; return Discourse.Topic.find(this.get('id'), {
if (diff === 0) { nearPost: opts.nearPost,
return false; bestOf: opts.bestOf,
} trackVisit: opts.trackVisit
}).then(function(result) {
// If loading the topic succeeded...
// Update the slug if different
var closestPostNumber, lastPost, postDiff;
if (result.slug) {
_this.set('slug', result.slug);
}
// If we want to scroll to a post that doesn't exist, just pop them to the closest
// one instead. This is likely happening due to a deleted post.
opts.nearPost = parseInt(opts.nearPost, 10);
closestPostNumber = 0;
postDiff = Number.MAX_VALUE;
result.posts.each(function(p) {
var diff;
diff = Math.abs(p.post_number - opts.nearPost);
if (diff < postDiff) {
postDiff = diff;
closestPostNumber = p.post_number;
if (diff === 0) {
return false;
} }
});
opts.nearPost = closestPostNumber;
if (_this.get('participants')) {
_this.get('participants').clear();
} }
if (result.suggested_topics) {
_this.set('suggested_topics', Em.A());
}
_this.mergeAttributes(result, {
suggested_topics: Discourse.Topic
});
_this.set('posts', Em.A());
if (opts.trackVisit && result.draft && result.draft.length > 0) {
Discourse.openComposer({
draft: Discourse.Draft.getLocal(result.draft_key, result.draft),
draftKey: result.draft_key,
draftSequence: result.draft_sequence,
topic: _this,
ignoreIfChanged: true
});
}
/* Okay this is weird, but let's store the length of the next post
*/
/* when there
*/
lastPost = null;
result.posts.each(function(p) {
var post;
p.scrollToAfterInsert = opts.nearPost;
post = Discourse.Post.create(p);
post.set('topic', _this);
_this.get('posts').pushObject(post);
lastPost = post;
});
return _this.set('loaded', true);
}, function(result) {
_this.set('missing', true);
return _this.set('message', Em.String.i18n('topic.not_found.description'));
}); });
},
notificationReasonText: (function() { opts.nearPost = closestPostNumber;
var locale_string; if (_this.get('participants')) {
locale_string = "topic.notifications.reasons." + this.notification_level; _this.get('participants').clear();
if (typeof this.notifications_reason_id === 'number') {
locale_string += "_" + this.notifications_reason_id;
} }
return Em.String.i18n(locale_string, { if (result.suggested_topics) {
username: Discourse.currentUser.username.toLowerCase() _this.set('suggested_topics', Em.A());
});
}).property('notifications_reason_id'),
updateNotifications: function(v) {
this.set('notification_level', v);
this.set('notifications_reason_id', null);
return jQuery.ajax({
url: "/t/" + (this.get('id')) + "/notifications",
type: 'POST',
data: {
notification_level: v
}
});
},
/* use to add post to topics protecting from dupes
*/
pushPosts: function(newPosts) {
var map, posts;
map = {};
posts = this.get('posts');
posts.each(function(p) {
map["" + p.post_number] = true;
});
return newPosts.each(function(p) {
if (!map[p.get('post_number')]) {
return posts.pushObject(p);
}
});
},
/* Is the reply to a post directly below it?
*/
isReplyDirectlyBelow: function(post) {
var postBelow, posts;
posts = this.get('posts');
if (!posts) {
return;
} }
postBelow = posts[posts.indexOf(post) + 1]; _this.mergeAttributes(result, {
/* If the post directly below's reply_to_post_number is our post number, it's suggested_topics: Discourse.Topic
considered directly below. */ });
return (postBelow ? postBelow.get('reply_to_post_number') : void 0) === post.get('post_number'); _this.set('posts', Em.A());
if (opts.trackVisit && result.draft && result.draft.length > 0) {
Discourse.openComposer({
draft: Discourse.Draft.getLocal(result.draft_key, result.draft),
draftKey: result.draft_key,
draftSequence: result.draft_sequence,
topic: _this,
ignoreIfChanged: true
});
}
// Okay this is weird, but let's store the length of the next post when there
lastPost = null;
result.posts.each(function(p) {
var post;
p.scrollToAfterInsert = opts.nearPost;
post = Discourse.Post.create(p);
post.set('topic', _this);
_this.get('posts').pushObject(post);
lastPost = post;
});
return _this.set('loaded', true);
}, function(result) {
_this.set('missing', true);
return _this.set('message', Em.String.i18n('topic.not_found.description'));
});
},
notificationReasonText: (function() {
var locale_string;
locale_string = "topic.notifications.reasons." + this.notification_level;
if (typeof this.notifications_reason_id === 'number') {
locale_string += "_" + this.notifications_reason_id;
} }
}); return Em.String.i18n(locale_string, { username: Discourse.currentUser.username.toLowerCase() });
}).property('notifications_reason_id'),
window.Discourse.Topic.reopenClass({ updateNotifications: function(v) {
NotificationLevel: { this.set('notification_level', v);
WATCHING: 3, this.set('notifications_reason_id', null);
TRACKING: 2, return jQuery.ajax({
REGULAR: 1, url: "/t/" + (this.get('id')) + "/notifications",
MUTE: 0 type: 'POST',
}, data: {
/* Load a topic, but accepts a set of filters notification_level: v
*/
/* options:
*/
/* onLoad - the callback after the topic is loaded
*/
find: function(topicId, opts) {
var data, promise, url,
_this = this;
url = "/t/" + topicId;
if (opts.nearPost) {
url += "/" + opts.nearPost;
} }
data = {}; });
if (opts.postsAfter) { },
data.posts_after = opts.postsAfter;
}
if (opts.postsBefore) {
data.posts_before = opts.postsBefore;
}
if (opts.trackVisit) {
data.track_visit = true;
}
/* Add username filters if we have them
*/
if (opts.userFilters && opts.userFilters.length > 0) { // use to add post to topics protecting from dupes
data.username_filters = []; pushPosts: function(newPosts) {
opts.userFilters.forEach(function(username) { var map, posts;
return data.username_filters.push(username); map = {};
}); posts = this.get('posts');
posts.each(function(p) {
map["" + p.post_number] = true;
});
return newPosts.each(function(p) {
if (!map[p.get('post_number')]) {
return posts.pushObject(p);
} }
/* Add the best of filter if we have it });
*/ },
if (opts.bestOf === true) { // Is the reply to a post directly below it?
data.best_of = true; isReplyDirectlyBelow: function(post) {
} var postBelow, posts;
/* Check the preload store. If not, load it via JSON posts = this.get('posts');
*/ if (!posts) {
return;
}
postBelow = posts[posts.indexOf(post) + 1];
// If the post directly below's reply_to_post_number is our post number, it's
// considered directly below.
return (postBelow ? postBelow.get('reply_to_post_number') : void 0) === post.get('post_number');
}
});
promise = new RSVP.Promise(); window.Discourse.Topic.reopenClass({
PreloadStore.get("topic_" + topicId, function() { NotificationLevel: {
return jQuery.getJSON(url + ".json", data); WATCHING: 3,
}).then(function(result) { TRACKING: 2,
var first; REGULAR: 1,
first = result.posts.first(); MUTE: 0
if (first && opts && opts.bestOf) { },
first.bestOfFirst = true;
}
return promise.resolve(result);
}, function(result) {
return promise.reject(result);
});
return promise;
},
/* Create a topic from posts
*/
movePosts: function(topicId, title, postIds) { // Load a topic, but accepts a set of filters
return jQuery.ajax("/t/" + topicId + "/move-posts", { // options:
type: 'POST', // onLoad - the callback after the topic is loaded
data: { find: function(topicId, opts) {
title: title, var data, promise, url,
post_ids: postIds _this = this;
} url = "/t/" + topicId;
}); if (opts.nearPost) {
}, url += "/" + opts.nearPost;
create: function(obj, topicView) { }
var _this = this; data = {};
return Object.tap(this._super(obj), function(result) { if (opts.postsAfter) {
if (result.participants) { data.posts_after = opts.postsAfter;
result.participants = result.participants.map(function(u) { }
return Discourse.User.create(u); if (opts.postsBefore) {
}); data.posts_before = opts.postsBefore;
result.fewParticipants = Em.A(); }
return result.participants.each(function(p) { if (opts.trackVisit) {
if (result.fewParticipants.length >= 8) { data.track_visit = true;
return false; }
}
result.fewParticipants.pushObject(p); // Add username filters if we have them
return true; if (opts.userFilters && opts.userFilters.length > 0) {
}); data.username_filters = [];
} opts.userFilters.forEach(function(username) {
return data.username_filters.push(username);
}); });
} }
});
}).call(this); // Add the best of filter if we have it
if (opts.bestOf === true) {
data.best_of = true;
}
// Check the preload store. If not, load it via JSON
promise = new RSVP.Promise();
PreloadStore.get("topic_" + topicId, function() {
return jQuery.getJSON(url + ".json", data);
}).then(function(result) {
var first;
first = result.posts.first();
if (first && opts && opts.bestOf) {
first.bestOfFirst = true;
}
return promise.resolve(result);
}, function(result) {
return promise.reject(result);
});
return promise;
},
// Create a topic from posts
movePosts: function(topicId, title, postIds) {
return jQuery.ajax("/t/" + topicId + "/move-posts", {
type: 'POST',
data: {
title: title,
post_ids: postIds
}
});
},
create: function(obj, topicView) {
var _this = this;
return Object.tap(this._super(obj), function(result) {
if (result.participants) {
result.participants = result.participants.map(function(u) {
return Discourse.User.create(u);
});
result.fewParticipants = Em.A();
return result.participants.each(function(p) {
if (result.fewParticipants.length >= 8) {
return false;
}
result.fewParticipants.pushObject(p);
return true;
});
}
});
}
});

View File

@ -1,119 +1,130 @@
(function() { /**
A data model representing a list of topics
window.Discourse.TopicList = Discourse.Model.extend({ @class TopicList
loadMoreTopics: function() { @extends Discourse.Model
var moreUrl, promise, @namespace Discourse
_this = this; @module Discourse
promise = new RSVP.Promise(); **/
if (moreUrl = this.get('more_topics_url')) {
Discourse.replaceState("/" + (this.get('filter')) + "/more"); Discourse.TopicList = Discourse.Model.extend({
jQuery.ajax(moreUrl, {
success: function(result) { loadMoreTopics: function() {
var newTopics, topicIds, topics; var moreUrl, promise,
if (result) { _this = this;
newTopics = Discourse.TopicList.topicsFrom(result); promise = new RSVP.Promise();
topics = _this.get('topics'); if (moreUrl = this.get('more_topics_url')) {
topicIds = []; Discourse.replaceState("/" + (this.get('filter')) + "/more");
topics.each(function(t) { jQuery.ajax(moreUrl, {
topicIds[t.get('id')] = true; success: function(result) {
}); var newTopics, topicIds, topics;
newTopics.each(function(t) { if (result) {
if (!topicIds[t.get('id')]) { newTopics = Discourse.TopicList.topicsFrom(result);
return topics.pushObject(t); topics = _this.get('topics');
} topicIds = [];
}); topics.each(function(t) {
_this.set('more_topics_url', result.topic_list.more_topics_url); topicIds[t.get('id')] = true;
Discourse.set('transient.topicsList', _this); });
} newTopics.each(function(t) {
return promise.resolve(result.topic_list.more_topics_url ? true : false); if (!topicIds[t.get('id')]) {
return topics.pushObject(t);
}
});
_this.set('more_topics_url', result.topic_list.more_topics_url);
Discourse.set('transient.topicsList', _this);
} }
}); return promise.resolve(result.topic_list.more_topics_url ? true : false);
} else {
promise.resolve(false);
}
return promise;
},
insert: function(json) {
var newTopic;
newTopic = Discourse.TopicList.decodeTopic(json);
/* New Topics are always unseen
*/
newTopic.set('unseen', true);
newTopic.set('highlightAfterInsert', true);
return this.get('inserted').unshiftObject(newTopic);
}
});
window.Discourse.TopicList.reopenClass({
decodeTopic: function(result) {
var categories, topic, users;
categories = this.extractByKey(result.categories, Discourse.Category);
users = this.extractByKey(result.users, Discourse.User);
topic = result.topic_list_item;
topic.category = categories[topic.category];
topic.posters.each(function(p) {
p.user = users[p.user_id] || users[p.user];
});
return Discourse.Topic.create(topic);
},
topicsFrom: function(result) {
/* Stitch together our side loaded data
*/
var categories, topics, users;
categories = this.extractByKey(result.categories, Discourse.Category);
users = this.extractByKey(result.users, Discourse.User);
topics = Em.A();
result.topic_list.topics.each(function(ft) {
ft.category = categories[ft.category_id];
ft.posters.each(function(p) {
p.user = users[p.user_id];
});
return topics.pushObject(Discourse.Topic.create(ft));
});
return topics;
},
list: function(menuItem) {
var filter, found, list, promise, topic_list, url;
filter = menuItem.name;
topic_list = Discourse.TopicList.create();
topic_list.set('inserted', Em.A());
topic_list.set('filter', filter);
url = "/" + filter + ".json";
if (menuItem.filters && menuItem.filters.length > 0) {
url += "?exclude_category=" + menuItem.filters[0].substring(1);
}
if (list = Discourse.get('transient.topicsList')) {
if ((list.get('filter') === filter) && window.location.pathname.indexOf('more') > 0) {
promise = new RSVP.Promise();
list.set('loaded', true);
promise.resolve(list);
return promise;
} }
}
Discourse.set('transient.topicsList', null);
Discourse.set('transient.topicListScrollPos', null);
promise = new RSVP.Promise();
found = PreloadStore.contains('topic_list');
PreloadStore.get("topic_list", function() {
return jQuery.getJSON(url);
}).then(function(result) {
topic_list.set('topics', Discourse.TopicList.topicsFrom(result));
topic_list.set('can_create_topic', result.topic_list.can_create_topic);
topic_list.set('more_topics_url', result.topic_list.more_topics_url);
topic_list.set('filter_summary', result.topic_list.filter_summary);
topic_list.set('draft_key', result.topic_list.draft_key);
topic_list.set('draft_sequence', result.topic_list.draft_sequence);
topic_list.set('draft', result.topic_list.draft);
if (result.topic_list.filtered_category) {
topic_list.set('category', Discourse.Category.create(result.topic_list.filtered_category));
}
topic_list.set('loaded', true);
return promise.resolve(topic_list);
}); });
return promise; } else {
promise.resolve(false);
} }
}); return promise;
},
insert: function(json) {
var newTopic;
newTopic = Discourse.TopicList.decodeTopic(json);
/* New Topics are always unseen
*/
newTopic.set('unseen', true);
newTopic.set('highlightAfterInsert', true);
return this.get('inserted').unshiftObject(newTopic);
}
});
Discourse.TopicList.reopenClass({
decodeTopic: function(result) {
var categories, topic, users;
categories = this.extractByKey(result.categories, Discourse.Category);
users = this.extractByKey(result.users, Discourse.User);
topic = result.topic_list_item;
topic.category = categories[topic.category];
topic.posters.each(function(p) {
p.user = users[p.user_id] || users[p.user];
});
return Discourse.Topic.create(topic);
},
topicsFrom: function(result) {
// Stitch together our side loaded data
var categories, topics, users;
categories = this.extractByKey(result.categories, Discourse.Category);
users = this.extractByKey(result.users, Discourse.User);
topics = Em.A();
result.topic_list.topics.each(function(ft) {
ft.category = categories[ft.category_id];
ft.posters.each(function(p) {
p.user = users[p.user_id];
});
return topics.pushObject(Discourse.Topic.create(ft));
});
return topics;
},
list: function(menuItem) {
var filter, found, list, promise, topic_list, url;
filter = menuItem.name;
topic_list = Discourse.TopicList.create();
topic_list.set('inserted', Em.A());
topic_list.set('filter', filter);
url = "/" + filter + ".json";
if (menuItem.filters && menuItem.filters.length > 0) {
url += "?exclude_category=" + menuItem.filters[0].substring(1);
}
if (list = Discourse.get('transient.topicsList')) {
if ((list.get('filter') === filter) && window.location.pathname.indexOf('more') > 0) {
promise = new RSVP.Promise();
list.set('loaded', true);
promise.resolve(list);
return promise;
}
}
Discourse.set('transient.topicsList', null);
Discourse.set('transient.topicListScrollPos', null);
promise = new RSVP.Promise();
found = PreloadStore.contains('topic_list');
PreloadStore.get("topic_list", function() {
return jQuery.getJSON(url);
}).then(function(result) {
topic_list.set('topics', Discourse.TopicList.topicsFrom(result));
topic_list.set('can_create_topic', result.topic_list.can_create_topic);
topic_list.set('more_topics_url', result.topic_list.more_topics_url);
topic_list.set('filter_summary', result.topic_list.filter_summary);
topic_list.set('draft_key', result.topic_list.draft_key);
topic_list.set('draft_sequence', result.topic_list.draft_sequence);
topic_list.set('draft', result.topic_list.draft);
if (result.topic_list.filtered_category) {
topic_list.set('category', Discourse.Category.create(result.topic_list.filtered_category));
}
topic_list.set('loaded', true);
return promise.resolve(topic_list);
});
return promise;
}
});
}).call(this);

View File

@ -1,311 +1,322 @@
(function() { /**
A data model representing a user on Discourse
window.Discourse.User = Discourse.Model.extend({ @class User
avatarLarge: (function() { @extends Discourse.Model
return Discourse.Utilities.avatarUrl(this.get('username'), 'large', this.get('avatar_template')); @namespace Discourse
}).property('username'), @module Discourse
avatarSmall: (function() { **/
return Discourse.Utilities.avatarUrl(this.get('username'), 'small', this.get('avatar_template')); Discourse.User = Discourse.Model.extend({
}).property('username'),
websiteName: (function() {
return this.get('website').split("/")[2];
}).property('website'),
path: (function() {
return "/users/" + (this.get('username_lower'));
}).property('username'),
username_lower: (function() {
return this.get('username').toLowerCase();
}).property('username'),
trustLevel: (function() {
return Discourse.get('site.trust_levels').findProperty('id', this.get('trust_level'));
}).property('trust_level'),
changeUsername: function(newUsername) {
return jQuery.ajax({
url: "/users/" + (this.get('username_lower')) + "/preferences/username",
type: 'PUT',
data: {
new_username: newUsername
}
});
},
changeEmail: function(email) {
return jQuery.ajax({
url: "/users/" + (this.get('username_lower')) + "/preferences/email",
type: 'PUT',
data: {
email: email
}
});
},
copy: function(deep) {
return Discourse.User.create(this.getProperties(Ember.keys(this)));
},
save: function(finished) {
var _this = this;
return jQuery.ajax("/users/" + this.get('username').toLowerCase(), {
data: this.getProperties('auto_track_topics_after_msecs',
'bio_raw',
'website',
'name',
'email_digests',
'email_direct',
'email_private_messages',
'digest_after_days',
'new_topic_duration_minutes'),
type: 'PUT',
success: function() {
return finished(true);
},
error: function() {
return finished(false);
}
});
},
changePassword: function(callback) {
var good;
good = false;
return jQuery.ajax({
url: '/session/forgot_password',
dataType: 'json',
data: {
username: this.get('username')
},
type: 'POST',
success: function() {
good = true;
},
complete: function() {
var message;
message = "error";
if (good) {
message = "email sent";
}
return callback(message);
}
});
},
filterStream: function(filter) {
if (Discourse.UserAction.statGroups[filter]) {
filter = Discourse.UserAction.statGroups[filter].join(",");
}
this.set('streamFilter', filter);
this.set('stream', Em.A());
return this.loadMoreUserActions();
},
loadUserAction: function(id) {
var stream,
_this = this;
stream = this.get('stream');
return jQuery.ajax({
url: "/user_actions/" + id + ".json",
dataType: 'json',
cache: 'false',
success: function(result) {
if (result) {
var action;
if ((_this.get('streamFilter') || result.action_type) !== result.action_type) {
return;
}
action = Em.A();
action.pushObject(Discourse.UserAction.create(result));
action = Discourse.UserAction.collapseStream(action);
return stream.insertAt(0, action[0]);
}
}
});
},
loadMoreUserActions: function(callback) {
var stream, url,
_this = this;
stream = this.get('stream');
if (!stream) {
return;
}
url = "/user_actions?offset=" + stream.length + "&user_id=" + (this.get("id"));
if (this.get('streamFilter')) {
url += "&filter=" + (this.get('streamFilter'));
}
return jQuery.ajax({
url: url,
dataType: 'json',
cache: 'false',
success: function(result) {
var copy;
if (result && result.user_actions && result.user_actions.each) {
copy = Em.A();
result.user_actions.each(function(i) {
return copy.pushObject(Discourse.UserAction.create(i));
});
copy = Discourse.UserAction.collapseStream(copy);
stream.pushObjects(copy);
_this.set('stream', stream);
}
if (callback) {
return callback();
}
}
});
},
statsCountNonPM: (function() {
var stats, total;
total = 0;
if (!(stats = this.get('stats'))) {
return 0;
}
this.get('stats').each(function(s) {
if (!s.get("isPM")) {
total += parseInt(s.count, 10);
}
});
return total;
}).property('stats.@each'),
statsExcludingPms: (function() {
var r;
r = [];
if (this.blank('stats')) {
return r;
}
this.get('stats').each(function(s) {
if (!s.get('isPM')) {
return r.push(s);
}
});
return r;
}).property('stats.@each'),
statsPmsOnly: (function() {
var r;
r = [];
if (this.blank('stats')) {
return r;
}
this.get('stats').each(function(s) {
if (s.get('isPM')) {
return r.push(s);
}
});
return r;
}).property('stats.@each'),
inboxCount: (function() {
var r;
r = 0;
this.get('stats').each(function(s) {
if (s.action_type === Discourse.UserAction.GOT_PRIVATE_MESSAGE) {
r = s.count;
return false;
}
});
return r;
}).property('stats.@each'),
sentItemsCount: (function() {
var r;
r = 0;
this.get('stats').each(function(s) {
if (s.action_type === Discourse.UserAction.NEW_PRIVATE_MESSAGE) {
r = s.count;
return false;
}
});
return r;
}).property('stats.@each')
});
window.Discourse.User.reopenClass({ avatarLarge: (function() {
checkUsername: function(username, email) { return Discourse.Utilities.avatarUrl(this.get('username'), 'large', this.get('avatar_template'));
return jQuery.ajax({ }).property('username'),
url: '/users/check_username',
type: 'GET',
data: {
username: username,
email: email
}
});
},
groupStats: function(stats) {
var g,
_this = this;
g = {};
stats.each(function(s) {
var c, found, k, v, _ref;
found = false;
_ref = Discourse.UserAction.statGroups;
for (k in _ref) {
v = _ref[k];
if (v.contains(s.action_type)) {
found = true;
if (!g[k]) {
g[k] = Em.Object.create({
description: Em.String.i18n("user_action_descriptions." + k),
count: 0,
action_type: parseInt(k, 10)
});
}
g[k].count += parseInt(s.count, 10);
c = g[k].count;
if (s.action_type === k) {
g[k] = s;
s.count = c;
}
}
}
if (!found) {
g[s.action_type] = s;
}
});
return stats.map(function(s) {
return g[s.action_type];
}).exclude(function(s) {
return !s;
});
},
find: function(username) {
var promise,
_this = this;
promise = new RSVP.Promise();
jQuery.ajax({
url: "/users/" + username + '.json',
success: function(json) {
/* todo: decompose to object
*/
var user; avatarSmall: (function() {
json.user.stats = _this.groupStats(json.user.stats.map(function(s) { return Discourse.Utilities.avatarUrl(this.get('username'), 'small', this.get('avatar_template'));
var obj; }).property('username'),
obj = Em.Object.create(s);
obj.isPM = obj.action_type === Discourse.UserAction.NEW_PRIVATE_MESSAGE || obj.action_type === Discourse.UserAction.GOT_PRIVATE_MESSAGE; websiteName: (function() {
return obj; return this.get('website').split("/")[2];
})); }).property('website'),
if (json.user.stream) {
json.user.stream = Discourse.UserAction.collapseStream(json.user.stream.map(function(ua) { path: (function() {
return Discourse.UserAction.create(ua); return "/users/" + (this.get('username_lower'));
})); }).property('username'),
}
user = Discourse.User.create(json.user); username_lower: (function() {
return promise.resolve(user); return this.get('username').toLowerCase();
}, }).property('username'),
error: function(xhr) {
return promise.reject(xhr); trustLevel: (function() {
return Discourse.get('site.trust_levels').findProperty('id', this.get('trust_level'));
}).property('trust_level'),
changeUsername: function(newUsername) {
return jQuery.ajax({
url: "/users/" + (this.get('username_lower')) + "/preferences/username",
type: 'PUT',
data: {
new_username: newUsername
}
});
},
changeEmail: function(email) {
return jQuery.ajax({
url: "/users/" + (this.get('username_lower')) + "/preferences/email",
type: 'PUT',
data: {
email: email
}
});
},
copy: function(deep) {
return Discourse.User.create(this.getProperties(Ember.keys(this)));
},
save: function(finished) {
var _this = this;
return jQuery.ajax("/users/" + this.get('username').toLowerCase(), {
data: this.getProperties('auto_track_topics_after_msecs',
'bio_raw',
'website',
'name',
'email_digests',
'email_direct',
'email_private_messages',
'digest_after_days',
'new_topic_duration_minutes'),
type: 'PUT',
success: function() { return finished(true); },
error: function() { return finished(false); }
});
},
changePassword: function(callback) {
var good;
good = false;
return jQuery.ajax({
url: '/session/forgot_password',
dataType: 'json',
data: {
username: this.get('username')
},
type: 'POST',
success: function() { good = true; },
complete: function() {
var message;
message = "error";
if (good) {
message = "email sent";
} }
}); return callback(message);
return promise; }
}, });
createAccount: function(name, email, password, username, passwordConfirm, challenge) { },
return jQuery.ajax({
url: '/users', filterStream: function(filter) {
dataType: 'json', if (Discourse.UserAction.statGroups[filter]) {
data: { filter = Discourse.UserAction.statGroups[filter].join(",");
name: name,
email: email,
password: password,
username: username,
password_confirmation: passwordConfirm,
challenge: challenge
},
type: 'POST'
});
} }
}); this.set('streamFilter', filter);
this.set('stream', Em.A());
return this.loadMoreUserActions();
},
}).call(this); loadUserAction: function(id) {
var stream,
_this = this;
stream = this.get('stream');
return jQuery.ajax({
url: "/user_actions/" + id + ".json",
dataType: 'json',
cache: 'false',
success: function(result) {
if (result) {
var action;
if ((_this.get('streamFilter') || result.action_type) !== result.action_type) {
return;
}
action = Em.A();
action.pushObject(Discourse.UserAction.create(result));
action = Discourse.UserAction.collapseStream(action);
return stream.insertAt(0, action[0]);
}
}
});
},
loadMoreUserActions: function(callback) {
var stream, url,
_this = this;
stream = this.get('stream');
if (!stream) return;
url = "/user_actions?offset=" + stream.length + "&user_id=" + (this.get("id"));
if (this.get('streamFilter')) {
url += "&filter=" + (this.get('streamFilter'));
}
return jQuery.ajax({
url: url,
dataType: 'json',
cache: 'false',
success: function(result) {
var copy;
if (result && result.user_actions && result.user_actions.each) {
copy = Em.A();
result.user_actions.each(function(i) {
return copy.pushObject(Discourse.UserAction.create(i));
});
copy = Discourse.UserAction.collapseStream(copy);
stream.pushObjects(copy);
_this.set('stream', stream);
}
if (callback) {
return callback();
}
}
});
},
statsCountNonPM: (function() {
var stats, total;
total = 0;
if (!(stats = this.get('stats'))) return 0;
this.get('stats').each(function(s) {
if (!s.get("isPM")) {
total += parseInt(s.count, 10);
}
});
return total;
}).property('stats.@each'),
statsExcludingPms: (function() {
var r;
r = [];
if (this.blank('stats')) return r;
this.get('stats').each(function(s) {
if (!s.get('isPM')) {
return r.push(s);
}
});
return r;
}).property('stats.@each'),
statsPmsOnly: (function() {
var r;
r = [];
if (this.blank('stats')) return r;
this.get('stats').each(function(s) {
if (s.get('isPM')) return r.push(s);
});
return r;
}).property('stats.@each'),
inboxCount: (function() {
var r;
r = 0;
this.get('stats').each(function(s) {
if (s.action_type === Discourse.UserAction.GOT_PRIVATE_MESSAGE) {
r = s.count;
return false;
}
});
return r;
}).property('stats.@each'),
sentItemsCount: (function() {
var r;
r = 0;
this.get('stats').each(function(s) {
if (s.action_type === Discourse.UserAction.NEW_PRIVATE_MESSAGE) {
r = s.count;
return false;
}
});
return r;
}).property('stats.@each')
});
Discourse.User.reopenClass({
checkUsername: function(username, email) {
return jQuery.ajax({
url: '/users/check_username',
type: 'GET',
data: {
username: username,
email: email
}
});
},
groupStats: function(stats) {
var g,
_this = this;
g = {};
stats.each(function(s) {
var c, found, k, v, _ref;
found = false;
_ref = Discourse.UserAction.statGroups;
for (k in _ref) {
v = _ref[k];
if (v.contains(s.action_type)) {
found = true;
if (!g[k]) {
g[k] = Em.Object.create({
description: Em.String.i18n("user_action_descriptions." + k),
count: 0,
action_type: parseInt(k, 10)
});
}
g[k].count += parseInt(s.count, 10);
c = g[k].count;
if (s.action_type === k) {
g[k] = s;
s.count = c;
}
}
}
if (!found) {
g[s.action_type] = s;
}
});
return stats.map(function(s) {
return g[s.action_type];
}).exclude(function(s) {
return !s;
});
},
find: function(username) {
var promise,
_this = this;
promise = new RSVP.Promise();
jQuery.ajax({
url: "/users/" + username + '.json',
success: function(json) {
// todo: decompose to object
var user;
json.user.stats = _this.groupStats(json.user.stats.map(function(s) {
var obj;
obj = Em.Object.create(s);
obj.isPM = obj.action_type === Discourse.UserAction.NEW_PRIVATE_MESSAGE || obj.action_type === Discourse.UserAction.GOT_PRIVATE_MESSAGE;
return obj;
}));
if (json.user.stream) {
json.user.stream = Discourse.UserAction.collapseStream(json.user.stream.map(function(ua) {
return Discourse.UserAction.create(ua);
}));
}
user = Discourse.User.create(json.user);
return promise.resolve(user);
},
error: function(xhr) {
return promise.reject(xhr);
}
});
return promise;
},
createAccount: function(name, email, password, username, passwordConfirm, challenge) {
return jQuery.ajax({
url: '/users',
dataType: 'json',
data: {
name: name,
email: email,
password: password,
username: username,
password_confirmation: passwordConfirm,
challenge: challenge
},
type: 'POST'
});
}
});

View File

@ -1,139 +1,150 @@
(function() { /**
A data model representing actions users have taken
window.Discourse.UserAction = Discourse.Model.extend({ @class UserAction
postUrl: (function() { @extends Discourse.Model
return Discourse.Utilities.postUrl(this.get('slug'), this.get('topic_id'), this.get('post_number')); @namespace Discourse
}).property(), @module Discourse
replyUrl: (function() { **/
return Discourse.Utilities.postUrl(this.get('slug'), this.get('topic_id'), this.get('reply_to_post_number')); Discourse.UserAction = Discourse.Model.extend({
}).property(),
isPM: (function() { postUrl: (function() {
var a; return Discourse.Utilities.postUrl(this.get('slug'), this.get('topic_id'), this.get('post_number'));
a = this.get('action_type'); }).property(),
return a === Discourse.UserAction.NEW_PRIVATE_MESSAGE || a === Discourse.UserAction.GOT_PRIVATE_MESSAGE;
}).property(), replyUrl: (function() {
isPostAction: (function() { return Discourse.Utilities.postUrl(this.get('slug'), this.get('topic_id'), this.get('reply_to_post_number'));
var a; }).property(),
a = this.get('action_type');
return a === Discourse.UserAction.RESPONSE || a === Discourse.UserAction.POST || a === Discourse.UserAction.NEW_TOPIC; isPM: (function() {
}).property(), var a = this.get('action_type');
addChild: function(action) { return a === Discourse.UserAction.NEW_PRIVATE_MESSAGE || a === Discourse.UserAction.GOT_PRIVATE_MESSAGE;
var bucket, current, groups, ua; }).property(),
groups = this.get("childGroups");
if (!groups) { isPostAction: (function() {
groups = { var a;
likes: Discourse.UserActionGroup.create({ a = this.get('action_type');
icon: "icon-heart" return a === Discourse.UserAction.RESPONSE || a === Discourse.UserAction.POST || a === Discourse.UserAction.NEW_TOPIC;
}), }).property(),
stars: Discourse.UserActionGroup.create({
icon: "icon-star" addChild: function(action) {
}), var bucket, current, groups, ua;
edits: Discourse.UserActionGroup.create({ groups = this.get("childGroups");
icon: "icon-pencil" if (!groups) {
}), groups = {
bookmarks: Discourse.UserActionGroup.create({ likes: Discourse.UserActionGroup.create({
icon: "icon-bookmark" icon: "icon-heart"
}) }),
}; stars: Discourse.UserActionGroup.create({
} icon: "icon-star"
this.set("childGroups", groups); }),
ua = Discourse.UserAction; edits: Discourse.UserActionGroup.create({
bucket = (function() { icon: "icon-pencil"
switch (action.action_type) { }),
case ua.LIKE: bookmarks: Discourse.UserActionGroup.create({
case ua.WAS_LIKED: icon: "icon-bookmark"
return "likes"; })
case ua.STAR: };
return "stars";
case ua.EDIT:
return "edits";
case ua.BOOKMARK:
return "bookmarks";
}
})();
current = groups[bucket];
if (current) {
current.push(action);
}
},
children: (function() {
var g, rval;
g = this.get("childGroups");
rval = [];
if (g) {
rval = [g.likes, g.stars, g.edits, g.bookmarks].filter(function(i) {
return i.get("items") && i.get("items").length > 0;
});
}
return rval;
}).property("childGroups"),
switchToActing: function() {
this.set('username', this.get('acting_username'));
this.set('avatar_template', this.get('acting_avatar_template'));
return this.set('name', this.get('acting_name'));
} }
}); this.set("childGroups", groups);
ua = Discourse.UserAction;
bucket = (function() {
switch (action.action_type) {
case ua.LIKE:
case ua.WAS_LIKED:
return "likes";
case ua.STAR:
return "stars";
case ua.EDIT:
return "edits";
case ua.BOOKMARK:
return "bookmarks";
}
})();
current = groups[bucket];
if (current) {
current.push(action);
}
},
window.Discourse.UserAction.reopenClass({ children: (function() {
collapseStream: function(stream) { var g, rval;
var collapse, collapsed, pos, uniq; g = this.get("childGroups");
collapse = [this.LIKE, this.WAS_LIKED, this.STAR, this.EDIT, this.BOOKMARK]; rval = [];
uniq = {}; if (g) {
collapsed = Em.A(); rval = [g.likes, g.stars, g.edits, g.bookmarks].filter(function(i) {
pos = 0; return i.get("items") && i.get("items").length > 0;
stream.each(function(item) {
var current, found, key;
key = "" + item.topic_id + "-" + item.post_number;
found = uniq[key];
if (found === void 0) {
if (collapse.indexOf(item.action_type) >= 0) {
current = Discourse.UserAction.create(item);
current.set('action_type', null);
current.set('description', null);
item.switchToActing();
current.addChild(item);
} else {
current = item;
}
uniq[key] = pos;
collapsed[pos] = current;
pos += 1;
} else {
if (collapse.indexOf(item.action_type) >= 0) {
item.switchToActing();
return collapsed[found].addChild(item);
} else {
collapsed[found].set('action_type', item.get('action_type'));
return collapsed[found].set('description', item.get('description'));
}
}
}); });
return collapsed; }
}, return rval;
/* in future we should be sending this through from the server }).property("childGroups"),
*/
LIKE: 1, switchToActing: function() {
WAS_LIKED: 2, this.set('username', this.get('acting_username'));
BOOKMARK: 3, this.set('avatar_template', this.get('acting_avatar_template'));
NEW_TOPIC: 4, this.set('name', this.get('acting_name'));
POST: 5, }
RESPONSE: 6, });
MENTION: 7,
QUOTE: 9, Discourse.UserAction.reopenClass({
STAR: 10, collapseStream: function(stream) {
EDIT: 11, var collapse, collapsed, pos, uniq;
NEW_PRIVATE_MESSAGE: 12, collapse = [this.LIKE, this.WAS_LIKED, this.STAR, this.EDIT, this.BOOKMARK];
GOT_PRIVATE_MESSAGE: 13 uniq = {};
}); collapsed = Em.A();
pos = 0;
stream.each(function(item) {
var current, found, key;
key = "" + item.topic_id + "-" + item.post_number;
found = uniq[key];
if (found === void 0) {
if (collapse.indexOf(item.action_type) >= 0) {
current = Discourse.UserAction.create(item);
current.set('action_type', null);
current.set('description', null);
item.switchToActing();
current.addChild(item);
} else {
current = item;
}
uniq[key] = pos;
collapsed[pos] = current;
pos += 1;
} else {
if (collapse.indexOf(item.action_type) >= 0) {
item.switchToActing();
return collapsed[found].addChild(item);
} else {
collapsed[found].set('action_type', item.get('action_type'));
return collapsed[found].set('description', item.get('description'));
}
}
});
return collapsed;
},
// in future we should be sending this through from the server
LIKE: 1,
WAS_LIKED: 2,
BOOKMARK: 3,
NEW_TOPIC: 4,
POST: 5,
RESPONSE: 6,
MENTION: 7,
QUOTE: 9,
STAR: 10,
EDIT: 11,
NEW_PRIVATE_MESSAGE: 12,
GOT_PRIVATE_MESSAGE: 13
});
window.Discourse.UserAction.reopenClass({
statGroups: (function() {
var g;
g = {};
g[Discourse.UserAction.RESPONSE] = [Discourse.UserAction.RESPONSE, Discourse.UserAction.MENTION, Discourse.UserAction.QUOTE];
return g;
})()
});
window.Discourse.UserAction.reopenClass({
statGroups: (function() {
var g;
g = {};
g[Discourse.UserAction.RESPONSE] = [Discourse.UserAction.RESPONSE, Discourse.UserAction.MENTION, Discourse.UserAction.QUOTE];
return g;
})()
});
}).call(this);

View File

@ -1,12 +1,18 @@
(function() { /**
A data model representing a group of UserActions
window.Discourse.UserActionGroup = Discourse.Model.extend({ @class UserActionGroup
push: function(item) { @extends Discourse.Model
if (!this.items) { @namespace Discourse
this.items = []; @module Discourse
} **/
return this.items.push(item); Discourse.UserActionGroup = Discourse.Model.extend({
push: function(item) {
if (!this.items) {
this.items = [];
} }
}); return this.items.push(item);
}
});
}).call(this);

View File

@ -1,5 +1,11 @@
(function() { /**
A data model representing a statistic on a UserAction
@class UserActionStat
@extends Discourse.Model
@namespace Discourse
@module Discourse
**/
Discourse.UserActionStat = Discourse.Model.extend({});
window.Discourse.UserActionStat = Discourse.Model.extend({});
}).call(this);

Some files were not shown because too many files have changed in this diff Show More