mirror of
https://github.com/codeninjasllc/discourse.git
synced 2024-11-30 10:58:31 -05:00
Convert all CoffeeScript to Javascript. See:
http://meta.discourse.org/t/is-it-better-for-discourse-to-use-javascript-or-coffeescript/3153
This commit is contained in:
parent
68ad545f0f
commit
f661fa609e
407 changed files with 13226 additions and 8953 deletions
3
Gemfile
3
Gemfile
|
@ -66,8 +66,6 @@ gem 'discourse_emoji', path: 'vendor/gems/discourse_emoji'
|
|||
# in production environments by default.
|
||||
# allow everywhere for now cause we are allowing asset debugging in prd
|
||||
group :assets do
|
||||
gem 'coffee-rails'
|
||||
gem 'coffee-script' # need this to compile coffee on the fly
|
||||
gem 'sass'
|
||||
gem 'sass-rails'
|
||||
gem 'turbo-sprockets-rails3'
|
||||
|
@ -79,6 +77,7 @@ group :test do
|
|||
end
|
||||
|
||||
group :test, :development do
|
||||
gem 'guard-jshint-on-rails'
|
||||
gem 'certified'
|
||||
gem 'fabrication'
|
||||
gem 'guard-jasmine'
|
||||
|
|
|
@ -183,6 +183,9 @@ GEM
|
|||
guard (>= 1.1.0)
|
||||
multi_json
|
||||
thor
|
||||
guard-jshint-on-rails (0.0.2)
|
||||
guard (>= 1.0.0)
|
||||
jshint_on_rails (>= 1.0.2)
|
||||
guard-rspec (2.4.0)
|
||||
guard (>= 1.1)
|
||||
rspec (~> 2.11)
|
||||
|
@ -215,6 +218,7 @@ GEM
|
|||
jquery-rails (2.2.0)
|
||||
railties (>= 3.0, < 5.0)
|
||||
thor (>= 0.14, < 2.0)
|
||||
jshint_on_rails (1.0.2)
|
||||
json (1.7.7)
|
||||
jwt (0.1.5)
|
||||
multi_json (>= 1.0)
|
||||
|
@ -453,8 +457,6 @@ DEPENDENCIES
|
|||
binding_of_caller
|
||||
certified
|
||||
clockwork
|
||||
coffee-rails
|
||||
coffee-script
|
||||
discourse_emoji!
|
||||
discourse_plugin!
|
||||
em-redis
|
||||
|
@ -466,6 +468,7 @@ DEPENDENCIES
|
|||
fastimage
|
||||
fog
|
||||
guard-jasmine
|
||||
guard-jshint-on-rails
|
||||
guard-rspec
|
||||
guard-spork
|
||||
has_ip_address
|
||||
|
|
17
Guardfile
17
Guardfile
|
@ -22,9 +22,17 @@ else
|
|||
jasmine_options[:server_timeout] = 300
|
||||
end
|
||||
|
||||
guard 'jasmine', jasmine_options do watch(%r{spec/javascripts/spec\.(js\.coffee|js|coffee)$}) { "spec/javascripts" }
|
||||
watch(%r{spec/javascripts/.+_spec\.(js\.coffee|js|coffee)$})
|
||||
watch(%r{app/assets/javascripts/(.+?)\.(js\.coffee|js|coffee)$}) { "spec/javascripts" }
|
||||
guard 'jasmine', jasmine_options do watch(%r{spec/javascripts/spec\.js$}) { "spec/javascripts" }
|
||||
watch(%r{spec/javascripts/.+_spec\.js$})
|
||||
watch(%r{app/assets/javascripts/(.+?)\.js$}) { "spec/javascripts" }
|
||||
end
|
||||
|
||||
# verify that we pass jshint
|
||||
# see https://github.com/MrOrz/guard-jshint-on-rails
|
||||
guard 'jshint-on-rails', config_path: 'config/jshint.yml' do
|
||||
# watch for changes to application javascript files
|
||||
watch(%r{^app/assets/javascripts/.*\.js$})
|
||||
watch(%r{^spec/javascripts/.*\.js$})
|
||||
end
|
||||
|
||||
guard 'rspec', :focus_on_failed => true, :cli => "--drb" do
|
||||
|
@ -45,6 +53,7 @@ guard 'rspec', :focus_on_failed => true, :cli => "--drb" do
|
|||
|
||||
end
|
||||
|
||||
|
||||
module ::Guard
|
||||
class AutoReload < ::Guard::Guard
|
||||
|
||||
|
@ -85,3 +94,5 @@ guard :autoreload do
|
|||
watch(/\.sass\.erb$/)
|
||||
watch(/\.handlebars$/)
|
||||
end
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
(function() {
|
||||
|
||||
window.Discourse.AdminCustomizeController = Ember.Controller.extend({
|
||||
newCustomization: function() {
|
||||
var item;
|
||||
item = Discourse.SiteCustomization.create({
|
||||
name: 'New Style'
|
||||
});
|
||||
this.get('content').pushObject(item);
|
||||
return this.set('content.selectedItem', item);
|
||||
},
|
||||
selectStyle: function(style) {
|
||||
return this.set('content.selectedItem', style);
|
||||
},
|
||||
save: function() {
|
||||
return this.get('content.selectedItem').save();
|
||||
},
|
||||
"delete": 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);
|
|
@ -1,18 +0,0 @@
|
|||
window.Discourse.AdminCustomizeController = Ember.Controller.extend
|
||||
newCustomization: ->
|
||||
item = Discourse.SiteCustomization.create(name: 'New Style')
|
||||
@get('content').pushObject(item)
|
||||
@set('content.selectedItem', item)
|
||||
|
||||
selectStyle: (style)-> @set('content.selectedItem', style)
|
||||
|
||||
save: -> @get('content.selectedItem').save()
|
||||
|
||||
delete: ->
|
||||
bootbox.confirm Em.String.i18n("admin.customize.delete_confirm"), Em.String.i18n("no_value"), Em.String.i18n("yes_value"), (result) =>
|
||||
if result
|
||||
selected = @get('content.selectedItem')
|
||||
selected.delete()
|
||||
@set('content.selectedItem', null)
|
||||
@get('content').removeObject(selected)
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
(function() {
|
||||
|
||||
window.Discourse.AdminDashboardController = Ember.Controller.extend({
|
||||
loading: true,
|
||||
versionCheck: null,
|
||||
upToDate: (function() {
|
||||
if (this.versionCheck) {
|
||||
return this.versionCheck.latest_version === this.versionCheck.installed_version;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}).property('versionCheck'),
|
||||
updateIconClasses: (function() {
|
||||
var classes;
|
||||
classes = "icon icon-warning-sign ";
|
||||
if (this.get('versionCheck.critical_updates')) {
|
||||
classes += "critical-updates-available";
|
||||
} else {
|
||||
classes += "updates-available";
|
||||
}
|
||||
return classes;
|
||||
}).property('versionCheck'),
|
||||
priorityClass: (function() {
|
||||
if (this.get('versionCheck.critical_updates')) {
|
||||
return 'version-check critical';
|
||||
} else {
|
||||
return 'version-check normal';
|
||||
}
|
||||
}).property('versionCheck')
|
||||
});
|
||||
|
||||
}).call(this);
|
|
@ -1,26 +0,0 @@
|
|||
window.Discourse.AdminDashboardController = Ember.Controller.extend
|
||||
loading: true
|
||||
versionCheck: null
|
||||
|
||||
upToDate: (->
|
||||
if @versionCheck
|
||||
@versionCheck.latest_version == @versionCheck.installed_version
|
||||
else
|
||||
true
|
||||
).property('versionCheck')
|
||||
|
||||
updateIconClasses: (->
|
||||
classes = "icon icon-warning-sign "
|
||||
if @get('versionCheck.critical_updates')
|
||||
classes += "critical-updates-available"
|
||||
else
|
||||
classes += "updates-available"
|
||||
classes
|
||||
).property('versionCheck')
|
||||
|
||||
priorityClass: (->
|
||||
if @get('versionCheck.critical_updates')
|
||||
'version-check critical'
|
||||
else
|
||||
'version-check normal'
|
||||
).property('versionCheck')
|
|
@ -0,0 +1,24 @@
|
|||
(function() {
|
||||
|
||||
window.Discourse.AdminEmailLogsController = Ember.ArrayController.extend(Discourse.Presence, {
|
||||
sendTestEmailDisabled: (function() {
|
||||
return this.blank('testEmailAddress');
|
||||
}).property('testEmailAddress'),
|
||||
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);
|
|
@ -1,17 +0,0 @@
|
|||
window.Discourse.AdminEmailLogsController = Ember.ArrayController.extend Discourse.Presence,
|
||||
|
||||
sendTestEmailDisabled: (->
|
||||
@blank('testEmailAddress')
|
||||
).property('testEmailAddress')
|
||||
|
||||
sendTestEmail: ->
|
||||
@set('sentTestEmail', false)
|
||||
$.ajax
|
||||
url: '/admin/email_logs/test',
|
||||
type: 'POST'
|
||||
data:
|
||||
email_address: @get('testEmailAddress')
|
||||
success: =>
|
||||
@set('sentTestEmail', true)
|
||||
false
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
(function() {
|
||||
|
||||
window.Discourse.AdminFlagsController = Ember.Controller.extend({
|
||||
clearFlags: function(item) {
|
||||
var _this = this;
|
||||
return item.clearFlags().then((function() {
|
||||
return _this.content.removeObject(item);
|
||||
}), (function() {
|
||||
return bootbox.alert("something went wrong");
|
||||
}));
|
||||
},
|
||||
deletePost: function(item) {
|
||||
var _this = this;
|
||||
return item.deletePost().then((function() {
|
||||
return _this.content.removeObject(item);
|
||||
}), (function() {
|
||||
return bootbox.alert("something went wrong");
|
||||
}));
|
||||
},
|
||||
adminOldFlagsView: (function() {
|
||||
return this.query === 'old';
|
||||
}).property('query'),
|
||||
adminActiveFlagsView: (function() {
|
||||
return this.query === 'active';
|
||||
}).property('query')
|
||||
});
|
||||
|
||||
}).call(this);
|
|
@ -1,23 +0,0 @@
|
|||
window.Discourse.AdminFlagsController = Ember.Controller.extend
|
||||
|
||||
clearFlags: (item) ->
|
||||
item.clearFlags().then (=>
|
||||
@content.removeObject(item)
|
||||
), (->
|
||||
bootbox.alert("something went wrong")
|
||||
)
|
||||
|
||||
deletePost: (item) ->
|
||||
item.deletePost().then (=>
|
||||
@content.removeObject(item)
|
||||
), (->
|
||||
bootbox.alert("something went wrong")
|
||||
)
|
||||
|
||||
adminOldFlagsView: (->
|
||||
@query == 'old'
|
||||
).property('query')
|
||||
|
||||
adminActiveFlagsView: (->
|
||||
@query == 'active'
|
||||
).property('query')
|
|
@ -0,0 +1,47 @@
|
|||
(function() {
|
||||
|
||||
window.Discourse.AdminSiteSettingsController = Ember.ArrayController.extend(Discourse.Presence, {
|
||||
filter: null,
|
||||
onlyOverridden: false,
|
||||
filteredContent: (function() {
|
||||
var filter,
|
||||
_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) {
|
||||
if (_this.get('onlyOverridden') && !item.get('overridden')) {
|
||||
return false;
|
||||
}
|
||||
if (filter) {
|
||||
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;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}).property('filter', 'content.@each', 'onlyOverridden'),
|
||||
resetDefault: function(setting) {
|
||||
setting.set('value', setting.get('default'));
|
||||
return setting.save();
|
||||
},
|
||||
save: function(setting) {
|
||||
return setting.save();
|
||||
},
|
||||
cancel: function(setting) {
|
||||
return setting.resetValue();
|
||||
}
|
||||
});
|
||||
|
||||
}).call(this);
|
|
@ -1,30 +0,0 @@
|
|||
window.Discourse.AdminSiteSettingsController = Ember.ArrayController.extend Discourse.Presence,
|
||||
|
||||
filter: null
|
||||
onlyOverridden: false
|
||||
|
||||
filteredContent: (->
|
||||
return null unless @present('content')
|
||||
filter = @get('filter').toLowerCase() if @get('filter')
|
||||
|
||||
@get('content').filter (item, index, enumerable) =>
|
||||
|
||||
return false if @get('onlyOverridden') and !item.get('overridden')
|
||||
|
||||
if filter
|
||||
return true 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 false
|
||||
else
|
||||
true
|
||||
).property('filter', 'content.@each', 'onlyOverridden')
|
||||
|
||||
|
||||
resetDefault: (setting) ->
|
||||
setting.set('value', setting.get('default'))
|
||||
setting.save()
|
||||
|
||||
save: (setting) -> setting.save()
|
||||
|
||||
cancel: (setting) -> setting.resetValue()
|
|
@ -0,0 +1,55 @@
|
|||
(function() {
|
||||
|
||||
window.Discourse.AdminUsersListController = Ember.ArrayController.extend(Discourse.Presence, {
|
||||
username: null,
|
||||
query: null,
|
||||
selectAll: false,
|
||||
content: null,
|
||||
selectAllChanged: (function() {
|
||||
var _this = this;
|
||||
return this.get('content').each(function(user) {
|
||||
return user.set('selected', _this.get('selectAll'));
|
||||
});
|
||||
}).observes('selectAll'),
|
||||
filterUsers: Discourse.debounce(function() {
|
||||
return this.refreshUsers();
|
||||
}, 250).observes('username'),
|
||||
orderChanged: (function() {
|
||||
return this.refreshUsers();
|
||||
}).observes('query'),
|
||||
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'),
|
||||
selectedCount: (function() {
|
||||
if (this.blank('content')) {
|
||||
return 0;
|
||||
}
|
||||
return this.get('content').filterProperty('selected').length;
|
||||
}).property('content.@each.selected'),
|
||||
hasSelection: (function() {
|
||||
return this.get('selectedCount') > 0;
|
||||
}).property('selectedCount'),
|
||||
refreshUsers: function() {
|
||||
return this.set('content', Discourse.AdminUser.findAll(this.get('query'), this.get('username')));
|
||||
},
|
||||
show: function(term) {
|
||||
if (this.get('query') === term) {
|
||||
return this.refreshUsers();
|
||||
} else {
|
||||
return this.set('query', term);
|
||||
}
|
||||
},
|
||||
approveUsers: function() {
|
||||
return Discourse.AdminUser.bulkApprove(this.get('content').filterProperty('selected'));
|
||||
}
|
||||
});
|
||||
|
||||
}).call(this);
|
|
@ -1,45 +0,0 @@
|
|||
window.Discourse.AdminUsersListController = Ember.ArrayController.extend Discourse.Presence,
|
||||
|
||||
username: null
|
||||
query: null
|
||||
selectAll: false
|
||||
content: null
|
||||
|
||||
selectAllChanged: (->
|
||||
@get('content').each (user) => user.set('selected', @get('selectAll'))
|
||||
).observes('selectAll')
|
||||
|
||||
filterUsers: Discourse.debounce(->
|
||||
@refreshUsers()
|
||||
,250).observes('username')
|
||||
|
||||
orderChanged: (->
|
||||
@refreshUsers()
|
||||
).observes('query')
|
||||
|
||||
showApproval: (->
|
||||
return false unless Discourse.SiteSettings.must_approve_users
|
||||
return true if @get('query') is 'new'
|
||||
return true if @get('query') is 'pending'
|
||||
).property('query')
|
||||
|
||||
selectedCount: (->
|
||||
return 0 if @blank('content')
|
||||
@get('content').filterProperty('selected').length
|
||||
).property('content.@each.selected')
|
||||
|
||||
hasSelection: (->
|
||||
@get('selectedCount') > 0
|
||||
).property('selectedCount')
|
||||
|
||||
refreshUsers: ->
|
||||
@set 'content', Discourse.AdminUser.findAll(@get('query'), @get('username'))
|
||||
|
||||
show: (term) ->
|
||||
if @get('query') == term
|
||||
@refreshUsers()
|
||||
else
|
||||
@set('query', term)
|
||||
|
||||
approveUsers: ->
|
||||
Discourse.AdminUser.bulkApprove(@get('content').filterProperty('selected'))
|
188
app/assets/javascripts/admin/models/admin_user.js
Normal file
188
app/assets/javascripts/admin/models/admin_user.js
Normal file
|
@ -0,0 +1,188 @@
|
|||
(function() {
|
||||
|
||||
window.Discourse.AdminUser = Discourse.Model.extend({
|
||||
deleteAllPosts: function() {
|
||||
this.set('can_delete_all_posts', false);
|
||||
return jQuery.ajax("/admin/users/" + (this.get('id')) + "/delete_all_posts", {
|
||||
type: 'PUT'
|
||||
});
|
||||
},
|
||||
/* Revoke the user's admin access
|
||||
*/
|
||||
|
||||
revokeAdmin: function() {
|
||||
this.set('admin', false);
|
||||
this.set('can_grant_admin', true);
|
||||
this.set('can_revoke_admin', false);
|
||||
return jQuery.ajax("/admin/users/" + (this.get('id')) + "/revoke_admin", {
|
||||
type: 'PUT'
|
||||
});
|
||||
},
|
||||
grantAdmin: function() {
|
||||
this.set('admin', true);
|
||||
this.set('can_grant_admin', false);
|
||||
this.set('can_revoke_admin', true);
|
||||
return jQuery.ajax("/admin/users/" + (this.get('id')) + "/grant_admin", {
|
||||
type: 'PUT'
|
||||
});
|
||||
},
|
||||
/* Revoke the user's moderation access
|
||||
*/
|
||||
|
||||
revokeModeration: function() {
|
||||
this.set('moderator', false);
|
||||
this.set('can_grant_moderation', true);
|
||||
this.set('can_revoke_moderation', false);
|
||||
return jQuery.ajax("/admin/users/" + (this.get('id')) + "/revoke_moderation", {
|
||||
type: 'PUT'
|
||||
});
|
||||
},
|
||||
grantModeration: function() {
|
||||
this.set('moderator', true);
|
||||
this.set('can_grant_moderation', false);
|
||||
this.set('can_revoke_moderation', true);
|
||||
return jQuery.ajax("/admin/users/" + (this.get('id')) + "/grant_moderation", {
|
||||
type: 'PUT'
|
||||
});
|
||||
},
|
||||
refreshBrowsers: function() {
|
||||
jQuery.ajax("/admin/users/" + (this.get('id')) + "/refresh_browsers", {
|
||||
type: 'POST'
|
||||
});
|
||||
return bootbox.alert("Message sent to all clients!");
|
||||
},
|
||||
approve: function() {
|
||||
this.set('can_approve', false);
|
||||
this.set('approved', true);
|
||||
this.set('approved_by', Discourse.get('currentUser'));
|
||||
return jQuery.ajax("/admin/users/" + (this.get('id')) + "/approve", {
|
||||
type: 'PUT'
|
||||
});
|
||||
},
|
||||
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'),
|
||||
canBan: (function() {
|
||||
return !this.admin && !this.moderator;
|
||||
}).property('admin', 'moderator'),
|
||||
banDuration: (function() {
|
||||
var banned_at, banned_till;
|
||||
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'),
|
||||
ban: function() {
|
||||
var duration,
|
||||
_this = this;
|
||||
if (duration = parseInt(window.prompt(Em.String.i18n('admin.user.ban_duration')), 10)) {
|
||||
if (duration > 0) {
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
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() {
|
||||
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'));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
window.Discourse.AdminUser.reopenClass({
|
||||
create: function(result) {
|
||||
result = this._super(result);
|
||||
return result;
|
||||
},
|
||||
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;
|
||||
})
|
||||
}
|
||||
});
|
||||
},
|
||||
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);
|
|
@ -1,137 +0,0 @@
|
|||
window.Discourse.AdminUser = Discourse.Model.extend
|
||||
|
||||
deleteAllPosts: ->
|
||||
@set('can_delete_all_posts', false)
|
||||
$.ajax "/admin/users/#{@get('id')}/delete_all_posts", type: 'PUT'
|
||||
|
||||
# Revoke the user's admin access
|
||||
revokeAdmin: ->
|
||||
@set('admin',false)
|
||||
@set('can_grant_admin',true)
|
||||
@set('can_revoke_admin',false)
|
||||
$.ajax "/admin/users/#{@get('id')}/revoke_admin", type: 'PUT'
|
||||
|
||||
grantAdmin: ->
|
||||
@set('admin',true)
|
||||
@set('can_grant_admin',false)
|
||||
@set('can_revoke_admin',true)
|
||||
$.ajax "/admin/users/#{@get('id')}/grant_admin", type: 'PUT'
|
||||
|
||||
# Revoke the user's moderation access
|
||||
revokeModeration: ->
|
||||
@set('moderator',false)
|
||||
@set('can_grant_moderation',true)
|
||||
@set('can_revoke_moderation',false)
|
||||
$.ajax "/admin/users/#{@get('id')}/revoke_moderation", type: 'PUT'
|
||||
|
||||
grantModeration: ->
|
||||
@set('moderator',true)
|
||||
@set('can_grant_moderation',false)
|
||||
@set('can_revoke_moderation',true)
|
||||
$.ajax "/admin/users/#{@get('id')}/grant_moderation", type: 'PUT'
|
||||
|
||||
refreshBrowsers: ->
|
||||
$.ajax "/admin/users/#{@get('id')}/refresh_browsers",
|
||||
type: 'POST'
|
||||
bootbox.alert("Message sent to all clients!")
|
||||
|
||||
approve: ->
|
||||
@set('can_approve', false)
|
||||
@set('approved', true)
|
||||
@set('approved_by', Discourse.get('currentUser'))
|
||||
$.ajax "/admin/users/#{@get('id')}/approve", type: 'PUT'
|
||||
|
||||
username_lower:(->
|
||||
@get('username').toLowerCase()
|
||||
).property('username')
|
||||
|
||||
trustLevel: (->
|
||||
Discourse.get('site.trust_levels').findProperty('id', @get('trust_level'))
|
||||
).property('trust_level')
|
||||
|
||||
|
||||
canBan: ( ->
|
||||
!@admin && !@moderator
|
||||
).property('admin','moderator')
|
||||
|
||||
banDuration: (->
|
||||
banned_at = Date.create(@banned_at)
|
||||
banned_till = Date.create(@banned_till)
|
||||
|
||||
"#{banned_at.short()} - #{banned_till.short()}"
|
||||
|
||||
).property('banned_till', 'banned_at')
|
||||
|
||||
ban: ->
|
||||
debugger
|
||||
if duration = parseInt(window.prompt(Em.String.i18n('admin.user.ban_duration')))
|
||||
if duration > 0
|
||||
$.ajax "/admin/users/#{@id}/ban",
|
||||
type: 'PUT'
|
||||
data:
|
||||
duration: duration
|
||||
success: ->
|
||||
window.location.reload()
|
||||
return
|
||||
error: (e) =>
|
||||
error = Em.String.i18n('admin.user.ban_failed', error: "http: #{e.status} - #{e.body}")
|
||||
bootbox.alert error
|
||||
return
|
||||
|
||||
unban: ->
|
||||
$.ajax "/admin/users/#{@id}/unban",
|
||||
type: 'PUT'
|
||||
success: ->
|
||||
window.location.reload()
|
||||
return
|
||||
error: (e) =>
|
||||
error = Em.String.i18n('admin.user.unban_failed', error: "http: #{e.status} - #{e.body}")
|
||||
bootbox.alert error
|
||||
return
|
||||
|
||||
impersonate: ->
|
||||
$.ajax "/admin/impersonate"
|
||||
type: 'POST'
|
||||
data:
|
||||
username_or_email: @get('username')
|
||||
success: ->
|
||||
document.location = "/"
|
||||
error: (e) =>
|
||||
@set('loading', false)
|
||||
if e.status == 404
|
||||
bootbox.alert Em.String.i18n('admin.impersonate.not_found')
|
||||
else
|
||||
bootbox.alert Em.String.i18n('admin.impersonate.invalid')
|
||||
|
||||
window.Discourse.AdminUser.reopenClass
|
||||
|
||||
create: (result) ->
|
||||
result = @_super(result)
|
||||
result
|
||||
|
||||
bulkApprove: (users) ->
|
||||
users.each (user) ->
|
||||
user.set('approved', true)
|
||||
user.set('can_approve', false)
|
||||
user.set('selected', false)
|
||||
|
||||
$.ajax "/admin/users/approve-bulk",
|
||||
type: 'PUT'
|
||||
data: {users: users.map (u) -> u.id}
|
||||
|
||||
find: (username)->
|
||||
promise = new RSVP.Promise()
|
||||
$.ajax
|
||||
url: "/admin/users/#{username}"
|
||||
success: (result) -> promise.resolve(Discourse.AdminUser.create(result))
|
||||
promise
|
||||
|
||||
findAll: (query, filter)->
|
||||
result = Em.A()
|
||||
$.ajax
|
||||
url: "/admin/users/list/#{query}.json"
|
||||
data: {filter: filter}
|
||||
success: (users) ->
|
||||
users.each (u) -> result.pushObject(Discourse.AdminUser.create(u))
|
||||
result
|
||||
|
30
app/assets/javascripts/admin/models/email_log.js
Normal file
30
app/assets/javascripts/admin/models/email_log.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
(function() {
|
||||
|
||||
window.Discourse.EmailLog = Discourse.Model.extend({});
|
||||
|
||||
window.Discourse.EmailLog.reopenClass({
|
||||
create: function(attrs) {
|
||||
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) {
|
||||
return logs.each(function(log) {
|
||||
return result.pushObject(Discourse.EmailLog.create(log));
|
||||
});
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
});
|
||||
|
||||
}).call(this);
|
|
@ -1,17 +0,0 @@
|
|||
window.Discourse.EmailLog = Discourse.Model.extend({})
|
||||
|
||||
window.Discourse.EmailLog.reopenClass
|
||||
|
||||
create: (attrs) ->
|
||||
attrs.user = Discourse.AdminUser.create(attrs.user) if attrs.user
|
||||
@_super(attrs)
|
||||
|
||||
findAll: (filter)->
|
||||
result = Em.A()
|
||||
$.ajax
|
||||
url: "/admin/email_logs.json"
|
||||
data: {filter: filter}
|
||||
success: (logs) ->
|
||||
logs.each (log) -> result.pushObject(Discourse.EmailLog.create(log))
|
||||
result
|
||||
|
109
app/assets/javascripts/admin/models/flagged_post.js
Normal file
109
app/assets/javascripts/admin/models/flagged_post.js
Normal file
|
@ -0,0 +1,109 @@
|
|||
(function() {
|
||||
|
||||
window.Discourse.FlaggedPost = Discourse.Post.extend({
|
||||
flaggers: (function() {
|
||||
var r,
|
||||
_this = this;
|
||||
r = [];
|
||||
this.post_actions.each(function(a) {
|
||||
return r.push(_this.userLookup[a.user_id]);
|
||||
});
|
||||
return r;
|
||||
}).property(),
|
||||
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() {
|
||||
return promise.resolve();
|
||||
},
|
||||
error: function(e) {
|
||||
return promise.reject();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return jQuery.ajax("/posts/" + this.id, {
|
||||
type: 'DELETE',
|
||||
cache: false,
|
||||
success: function() {
|
||||
return promise.resolve();
|
||||
},
|
||||
error: function(e) {
|
||||
return promise.reject();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
clearFlags: function() {
|
||||
var promise;
|
||||
promise = new RSVP.Promise();
|
||||
jQuery.ajax("/admin/flags/clear/" + this.id, {
|
||||
type: 'POST',
|
||||
cache: false,
|
||||
success: function() {
|
||||
return promise.resolve();
|
||||
},
|
||||
error: function(e) {
|
||||
return promise.reject();
|
||||
}
|
||||
});
|
||||
return promise;
|
||||
},
|
||||
hiddenClass: (function() {
|
||||
if (this.get('hidden') === "t") {
|
||||
return "hidden-post";
|
||||
}
|
||||
}).property()
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
}).call(this);
|
|
@ -1,81 +0,0 @@
|
|||
window.Discourse.FlaggedPost = Discourse.Post.extend
|
||||
flaggers: (->
|
||||
r = []
|
||||
@post_actions.each (a)=>
|
||||
r.push(@userLookup[a.user_id])
|
||||
r
|
||||
).property()
|
||||
|
||||
messages: (->
|
||||
r = []
|
||||
@post_actions.each (a)=>
|
||||
if a.message
|
||||
r.push
|
||||
user: @userLookup[a.user_id]
|
||||
message: a.message
|
||||
r
|
||||
).property()
|
||||
|
||||
lastFlagged: (->
|
||||
@post_actions[0].created_at
|
||||
).property()
|
||||
|
||||
user: (->
|
||||
@userLookup[@user_id]
|
||||
).property()
|
||||
|
||||
topicHidden: (->
|
||||
@get('topic_visible') == 'f'
|
||||
).property('topic_hidden')
|
||||
|
||||
deletePost: ->
|
||||
promise = new RSVP.Promise()
|
||||
if @get('post_number') == "1"
|
||||
$.ajax "/t/#{@topic_id}",
|
||||
type: 'DELETE'
|
||||
cache: false
|
||||
success: ->
|
||||
promise.resolve()
|
||||
error: (e)->
|
||||
promise.reject()
|
||||
else
|
||||
$.ajax "/posts/#{@id}",
|
||||
type: 'DELETE'
|
||||
cache: false
|
||||
success: ->
|
||||
promise.resolve()
|
||||
error: (e)->
|
||||
promise.reject()
|
||||
|
||||
clearFlags: ->
|
||||
promise = new RSVP.Promise()
|
||||
$.ajax "/admin/flags/clear/#{@id}",
|
||||
type: 'POST'
|
||||
cache: false
|
||||
success: ->
|
||||
promise.resolve()
|
||||
error: (e)->
|
||||
promise.reject()
|
||||
|
||||
promise
|
||||
|
||||
hiddenClass: (->
|
||||
"hidden-post" if @get('hidden') == "t"
|
||||
).property()
|
||||
|
||||
|
||||
window.Discourse.FlaggedPost.reopenClass
|
||||
|
||||
findAll: (filter) ->
|
||||
result = Em.A()
|
||||
$.ajax
|
||||
url: "/admin/flags/#{filter}.json"
|
||||
success: (data) ->
|
||||
userLookup = {}
|
||||
data.users.each (u) -> userLookup[u.id] = Discourse.User.create(u)
|
||||
data.posts.each (p) ->
|
||||
f = Discourse.FlaggedPost.create(p)
|
||||
f.userLookup = userLookup
|
||||
result.pushObject(f)
|
||||
result
|
||||
|
101
app/assets/javascripts/admin/models/site_customization.js
Normal file
101
app/assets/javascripts/admin/models/site_customization.js
Normal file
|
@ -0,0 +1,101 @@
|
|||
(function() {
|
||||
var SiteCustomizations;
|
||||
|
||||
window.Discourse.SiteCustomization = Discourse.Model.extend({
|
||||
init: function() {
|
||||
this._super();
|
||||
return this.startTrackingChanges();
|
||||
},
|
||||
trackedProperties: ['enabled', 'name', 'stylesheet', 'header', 'override_default_style'],
|
||||
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'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
}).call(this);
|
|
@ -1,78 +0,0 @@
|
|||
window.Discourse.SiteCustomization = Discourse.Model.extend
|
||||
|
||||
init: ->
|
||||
@_super()
|
||||
@startTrackingChanges()
|
||||
|
||||
trackedProperties: ['enabled','name', 'stylesheet', 'header', 'override_default_style']
|
||||
|
||||
description: (->
|
||||
"#{@name}#{if @enabled then ' (*)' else ''}"
|
||||
).property('selected', 'name')
|
||||
|
||||
changed: (->
|
||||
return false unless @originals
|
||||
@trackedProperties.any (p)=>
|
||||
@originals[p] != @get(p)
|
||||
).property('override_default_style','enabled','name', 'stylesheet', 'header', 'originals') # TODO figure out how to call with apply
|
||||
|
||||
startTrackingChanges: ->
|
||||
@set('originals',{})
|
||||
|
||||
@trackedProperties.each (p)=>
|
||||
@originals[p] = @get(p)
|
||||
true
|
||||
|
||||
previewUrl: (->
|
||||
"/?preview-style=#{@get('key')}"
|
||||
).property('key')
|
||||
|
||||
disableSave:(->
|
||||
!@get('changed')
|
||||
).property('changed')
|
||||
|
||||
save: ->
|
||||
@startTrackingChanges()
|
||||
data =
|
||||
name: @name
|
||||
enabled: @enabled
|
||||
stylesheet: @stylesheet
|
||||
header: @header
|
||||
override_default_style: @override_default_style
|
||||
|
||||
$.ajax
|
||||
url: "/admin/site_customizations#{if @id then '/' + @id else ''}"
|
||||
data:
|
||||
site_customization: data
|
||||
type: if @id then 'PUT' else 'POST'
|
||||
|
||||
delete: ->
|
||||
return unless @id
|
||||
$.ajax
|
||||
url: "/admin/site_customizations/#{ @id }"
|
||||
type: 'DELETE'
|
||||
|
||||
SiteCustomizations = Ember.ArrayProxy.extend
|
||||
selectedItemChanged: (->
|
||||
selected = @get('selectedItem')
|
||||
@get('content').each (i)->
|
||||
i.set('selected', selected == i)
|
||||
).observes('selectedItem')
|
||||
|
||||
|
||||
Discourse.SiteCustomization.reopenClass
|
||||
findAll: ->
|
||||
content = SiteCustomizations.create
|
||||
content: []
|
||||
loading: true
|
||||
|
||||
$.ajax
|
||||
url: "/admin/site_customizations"
|
||||
dataType: "json"
|
||||
success: (data)=>
|
||||
data?.site_customizations.each (c)->
|
||||
item = Discourse.SiteCustomization.create(c)
|
||||
content.pushObject(item)
|
||||
content.set('loading',false)
|
||||
|
||||
content
|
62
app/assets/javascripts/admin/models/site_setting.js
Normal file
62
app/assets/javascripts/admin/models/site_setting.js
Normal file
|
@ -0,0 +1,62 @@
|
|||
(function() {
|
||||
|
||||
window.Discourse.SiteSetting = Discourse.Model.extend(Discourse.Presence, {
|
||||
/* 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
|
||||
*/
|
||||
|
||||
dirty: (function() {
|
||||
return this.get('originalValue') !== this.get('value');
|
||||
}).property('originalValue', 'value'),
|
||||
overridden: (function() {
|
||||
var defaultVal, val;
|
||||
val = this.get('value');
|
||||
defaultVal = this.get('default');
|
||||
if (val && defaultVal) {
|
||||
return val.toString() !== defaultVal.toString();
|
||||
}
|
||||
return val !== defaultVal;
|
||||
}).property('value'),
|
||||
resetValue: function() {
|
||||
return this.set('value', this.get('originalValue'));
|
||||
},
|
||||
save: function() {
|
||||
/* Update the setting
|
||||
*/
|
||||
|
||||
var _this = this;
|
||||
return jQuery.ajax("/admin/site_settings/" + (this.get('setting')), {
|
||||
data: {
|
||||
value: this.get('value')
|
||||
},
|
||||
type: 'PUT',
|
||||
success: function() {
|
||||
return _this.set('originalValue', _this.get('value'));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
|
@ -1,42 +0,0 @@
|
|||
window.Discourse.SiteSetting = Discourse.Model.extend Discourse.Presence,
|
||||
|
||||
# Whether a property is short.
|
||||
short: (->
|
||||
return true if @blank('value')
|
||||
return @get('value').toString().length < 80
|
||||
).property('value')
|
||||
|
||||
# Whether the site setting has changed
|
||||
dirty: (->
|
||||
@get('originalValue') != @get('value')
|
||||
).property('originalValue', 'value')
|
||||
|
||||
overridden: (->
|
||||
val = @get('value')
|
||||
defaultVal = @get('default')
|
||||
return val.toString() != defaultVal.toString() if (val and defaultVal)
|
||||
return val != defaultVal
|
||||
).property('value')
|
||||
|
||||
resetValue: ->
|
||||
@set('value', @get('originalValue'))
|
||||
|
||||
save: ->
|
||||
|
||||
# Update the setting
|
||||
$.ajax "/admin/site_settings/#{@get('setting')}",
|
||||
data:
|
||||
value: @get('value')
|
||||
type: 'PUT'
|
||||
success: => @set('originalValue', @get('value'))
|
||||
|
||||
|
||||
window.Discourse.SiteSetting.reopenClass
|
||||
findAll: ->
|
||||
result = Em.A()
|
||||
$.get "/admin/site_settings", (settings) ->
|
||||
settings.each (s) ->
|
||||
s.originalValue = s.value
|
||||
result.pushObject(Discourse.SiteSetting.create(s))
|
||||
result
|
||||
|
18
app/assets/javascripts/admin/models/version_check.js
Normal file
18
app/assets/javascripts/admin/models/version_check.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
(function() {
|
||||
|
||||
window.Discourse.VersionCheck = Discourse.Model.extend({});
|
||||
|
||||
Discourse.VersionCheck.reopenClass({
|
||||
find: function() {
|
||||
var _this = this;
|
||||
return jQuery.ajax({
|
||||
url: '/admin/version_check',
|
||||
dataType: 'json',
|
||||
success: function(json) {
|
||||
return Discourse.VersionCheck.create(json);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
}).call(this);
|
|
@ -1,9 +0,0 @@
|
|||
window.Discourse.VersionCheck = Discourse.Model.extend({})
|
||||
|
||||
Discourse.VersionCheck.reopenClass
|
||||
find: ->
|
||||
$.ajax
|
||||
url: '/admin/version_check'
|
||||
dataType: 'json'
|
||||
success: (json) =>
|
||||
Discourse.VersionCheck.create(json)
|
|
@ -0,0 +1,9 @@
|
|||
(function() {
|
||||
|
||||
Discourse.AdminCustomizeRoute = Discourse.Route.extend({
|
||||
model: function() {
|
||||
return Discourse.SiteCustomization.findAll();
|
||||
}
|
||||
});
|
||||
|
||||
}).call(this);
|
|
@ -1,2 +0,0 @@
|
|||
Discourse.AdminCustomizeRoute = Discourse.Route.extend
|
||||
model: -> Discourse.SiteCustomization.findAll()
|
12
app/assets/javascripts/admin/routes/admin_dashboard_route.js
Normal file
12
app/assets/javascripts/admin/routes/admin_dashboard_route.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
(function() {
|
||||
|
||||
Discourse.AdminDashboardRoute = Discourse.Route.extend({
|
||||
setupController: function(c) {
|
||||
return Discourse.VersionCheck.find().then(function(vc) {
|
||||
c.set('versionCheck', vc);
|
||||
return c.set('loading', false);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
}).call(this);
|
|
@ -1,6 +0,0 @@
|
|||
Discourse.AdminDashboardRoute = Discourse.Route.extend
|
||||
setupController: (c) ->
|
||||
Discourse.VersionCheck.find().then (vc) ->
|
||||
# Loading finished!
|
||||
c.set('versionCheck', vc)
|
||||
c.set('loading', false)
|
|
@ -0,0 +1,9 @@
|
|||
(function() {
|
||||
|
||||
Discourse.AdminEmailLogsRoute = Discourse.Route.extend({
|
||||
model: function() {
|
||||
return Discourse.EmailLog.findAll();
|
||||
}
|
||||
});
|
||||
|
||||
}).call(this);
|
|
@ -1,2 +0,0 @@
|
|||
Discourse.AdminEmailLogsRoute = Discourse.Route.extend
|
||||
model: -> Discourse.EmailLog.findAll()
|
|
@ -0,0 +1,15 @@
|
|||
(function() {
|
||||
|
||||
Discourse.AdminFlagsActiveRoute = Discourse.Route.extend({
|
||||
model: function() {
|
||||
return Discourse.FlaggedPost.findAll('active');
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
var c;
|
||||
c = this.controllerFor('adminFlags');
|
||||
c.set('content', model);
|
||||
return c.set('query', 'active');
|
||||
}
|
||||
});
|
||||
|
||||
}).call(this);
|
|
@ -1,6 +0,0 @@
|
|||
Discourse.AdminFlagsActiveRoute = Discourse.Route.extend
|
||||
model: -> Discourse.FlaggedPost.findAll('active')
|
||||
setupController: (controller, model) ->
|
||||
c = @controllerFor('adminFlags')
|
||||
c.set('content', model)
|
||||
c.set('query', 'active')
|
15
app/assets/javascripts/admin/routes/admin_flags_old_route.js
Normal file
15
app/assets/javascripts/admin/routes/admin_flags_old_route.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
(function() {
|
||||
|
||||
Discourse.AdminFlagsOldRoute = Discourse.Route.extend({
|
||||
model: function() {
|
||||
return Discourse.FlaggedPost.findAll('old');
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
var c;
|
||||
c = this.controllerFor('adminFlags');
|
||||
c.set('content', model);
|
||||
return c.set('query', 'old');
|
||||
}
|
||||
});
|
||||
|
||||
}).call(this);
|
|
@ -1,6 +0,0 @@
|
|||
Discourse.AdminFlagsOldRoute = Discourse.Route.extend
|
||||
model: -> Discourse.FlaggedPost.findAll('old')
|
||||
setupController: (controller, model) ->
|
||||
c = @controllerFor('adminFlags')
|
||||
c.set('content', model)
|
||||
c.set('query', 'old')
|
52
app/assets/javascripts/admin/routes/admin_routes.js
Normal file
52
app/assets/javascripts/admin/routes/admin_routes.js
Normal file
|
@ -0,0 +1,52 @@
|
|||
(function() {
|
||||
|
||||
Discourse.buildRoutes(function() {
|
||||
return 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('active', {
|
||||
path: '/active'
|
||||
});
|
||||
return this.route('old', {
|
||||
path: '/old'
|
||||
});
|
||||
});
|
||||
return this.resource('adminUsers', {
|
||||
path: '/users'
|
||||
}, function() {
|
||||
this.resource('adminUser', {
|
||||
path: '/:username'
|
||||
});
|
||||
return this.resource('adminUsersList', {
|
||||
path: '/list'
|
||||
}, function() {
|
||||
this.route('active', {
|
||||
path: '/active'
|
||||
});
|
||||
this.route('new', {
|
||||
path: '/new'
|
||||
});
|
||||
return this.route('pending', {
|
||||
path: '/pending'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}).call(this);
|
|
@ -1,17 +0,0 @@
|
|||
Discourse.buildRoutes ->
|
||||
@resource 'admin', path: '/admin', ->
|
||||
@route 'dashboard', path: '/'
|
||||
@route 'site_settings', path: '/site_settings'
|
||||
@route 'email_logs', path: '/email_logs'
|
||||
@route 'customize', path: '/customize'
|
||||
|
||||
@resource 'adminFlags', path: '/flags', ->
|
||||
@route 'active', path: '/active'
|
||||
@route 'old', path: '/old'
|
||||
|
||||
@resource 'adminUsers', path: '/users', ->
|
||||
@resource 'adminUser', path: '/:username'
|
||||
@resource 'adminUsersList', path: '/list', ->
|
||||
@route 'active', path: '/active'
|
||||
@route 'new', path: '/new'
|
||||
@route 'pending', path: '/pending'
|
|
@ -0,0 +1,9 @@
|
|||
(function() {
|
||||
|
||||
Discourse.AdminSiteSettingsRoute = Discourse.Route.extend({
|
||||
model: function() {
|
||||
return Discourse.SiteSetting.findAll();
|
||||
}
|
||||
});
|
||||
|
||||
}).call(this);
|
|
@ -1,2 +0,0 @@
|
|||
Discourse.AdminSiteSettingsRoute = Discourse.Route.extend
|
||||
model: -> Discourse.SiteSetting.findAll()
|
9
app/assets/javascripts/admin/routes/admin_user_route.js
Normal file
9
app/assets/javascripts/admin/routes/admin_user_route.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
(function() {
|
||||
|
||||
Discourse.AdminUserRoute = Discourse.Route.extend({
|
||||
model: function(params) {
|
||||
return Discourse.AdminUser.find(params.username);
|
||||
}
|
||||
});
|
||||
|
||||
}).call(this);
|
|
@ -1,2 +0,0 @@
|
|||
Discourse.AdminUserRoute = Discourse.Route.extend
|
||||
model: (params) -> Discourse.AdminUser.find(params.username)
|
|
@ -0,0 +1,9 @@
|
|||
(function() {
|
||||
|
||||
Discourse.AdminUsersListActiveRoute = Discourse.Route.extend({
|
||||
setupController: function(c) {
|
||||
return this.controllerFor('adminUsersList').show('active');
|
||||
}
|
||||
});
|
||||
|
||||
}).call(this);
|
|
@ -1,2 +0,0 @@
|
|||
Discourse.AdminUsersListActiveRoute = Discourse.Route.extend
|
||||
setupController: (c) -> @controllerFor('adminUsersList').show('active')
|
|
@ -0,0 +1,9 @@
|
|||
(function() {
|
||||
|
||||
Discourse.AdminUsersListNewRoute = Discourse.Route.extend({
|
||||
setupController: function(c) {
|
||||
return this.controllerFor('adminUsersList').show('new');
|
||||
}
|
||||
});
|
||||
|
||||
}).call(this);
|
|
@ -1,2 +0,0 @@
|
|||
Discourse.AdminUsersListNewRoute = Discourse.Route.extend
|
||||
setupController: (c) -> @controllerFor('adminUsersList').show('new')
|
|
@ -0,0 +1,9 @@
|
|||
(function() {
|
||||
|
||||
Discourse.AdminUsersListNewRoute = Discourse.Route.extend({
|
||||
setupController: function(c) {
|
||||
return this.controllerFor('adminUsersList').show('pending');
|
||||
}
|
||||
});
|
||||
|
||||
}).call(this);
|
|
@ -1,2 +0,0 @@
|
|||
Discourse.AdminUsersListNewRoute = Discourse.Route.extend
|
||||
setupController: (c) -> @controllerFor('adminUsersList').show('pending')
|
|
@ -4,4 +4,4 @@
|
|||
<% admin = SimplesIdeias::I18n.translation_segments['app/assets/javascripts/i18n/admin.en.js']
|
||||
admin[:en][:js] = admin[:en].delete(:admin_js)
|
||||
%>
|
||||
$.extend(true, I18n.translations, <%= admin.to_json %>);
|
||||
jQuery.extend(true, I18n.translations, <%= admin.to_json %>);
|
||||
|
|
64
app/assets/javascripts/admin/views/ace_editor_view.js
Normal file
64
app/assets/javascripts/admin/views/ace_editor_view.js
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*global ace:true */
|
||||
(function() {
|
||||
|
||||
Discourse.AceEditorView = window.Discourse.View.extend({
|
||||
mode: 'css',
|
||||
classNames: ['ace-wrapper'],
|
||||
contentChanged: (function() {
|
||||
if (this.editor && !this.skipContentChangeEvent) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}).call(this);
|
|
@ -1,42 +0,0 @@
|
|||
Discourse.AceEditorView = window.Discourse.View.extend
|
||||
mode: 'css'
|
||||
classNames: ['ace-wrapper']
|
||||
|
||||
contentChanged:(->
|
||||
if @editor && !@skipContentChangeEvent
|
||||
@editor.getSession().setValue(@get('content'))
|
||||
).observes('content')
|
||||
|
||||
render: (buffer) ->
|
||||
buffer.push("<div class='ace'>")
|
||||
buffer.push(Handlebars.Utils.escapeExpression(@get('content'))) if @get('content')
|
||||
buffer.push("</div>")
|
||||
|
||||
willDestroyElement: ->
|
||||
if @editor
|
||||
@editor.destroy()
|
||||
@editor = null
|
||||
|
||||
didInsertElement: ->
|
||||
initAce = =>
|
||||
@editor = ace.edit(@$('.ace')[0])
|
||||
@editor.setTheme("ace/theme/chrome")
|
||||
@editor.setShowPrintMargin(false)
|
||||
@editor.getSession().setMode("ace/mode/#{@get('mode')}")
|
||||
@editor.on "change", (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
|
||||
|
||||
@skipContentChangeEvent = true
|
||||
@set('content', @editor.getSession().getValue())
|
||||
@skipContentChangeEvent = false
|
||||
if window.ace
|
||||
initAce()
|
||||
else
|
||||
$LAB.script('http://d1n0x3qji82z53.cloudfront.net/src-min-noconflict/ace.js').wait initAce
|
||||
|
||||
|
36
app/assets/javascripts/admin/views/admin_customize_view.js
Normal file
36
app/assets/javascripts/admin/views/admin_customize_view.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
/*global Mousetrap:true */
|
||||
(function() {
|
||||
|
||||
Discourse.AdminCustomizeView = window.Discourse.View.extend({
|
||||
templateName: 'admin/templates/customize',
|
||||
classNames: ['customize'],
|
||||
contentBinding: 'controller.content',
|
||||
init: function() {
|
||||
this._super();
|
||||
return this.set('selected', 'stylesheet');
|
||||
},
|
||||
headerActive: (function() {
|
||||
return this.get('selected') === 'header';
|
||||
}).property('selected'),
|
||||
stylesheetActive: (function() {
|
||||
return this.get('selected') === 'stylesheet';
|
||||
}).property('selected'),
|
||||
selectHeader: function() {
|
||||
return this.set('selected', 'header');
|
||||
},
|
||||
selectStylesheet: function() {
|
||||
return this.set('selected', 'stylesheet');
|
||||
},
|
||||
didInsertElement: function() {
|
||||
var _this = this;
|
||||
return Mousetrap.bindGlobal(['meta+s', 'ctrl+s'], function() {
|
||||
_this.get('controller').save();
|
||||
return false;
|
||||
});
|
||||
},
|
||||
willDestroyElement: function() {
|
||||
return Mousetrap.unbindGlobal('meta+s', 'ctrl+s');
|
||||
}
|
||||
});
|
||||
|
||||
}).call(this);
|
|
@ -1,33 +0,0 @@
|
|||
Discourse.AdminCustomizeView = window.Discourse.View.extend
|
||||
templateName: 'admin/templates/customize'
|
||||
classNames: ['customize']
|
||||
contentBinding: 'controller.content'
|
||||
|
||||
init: ->
|
||||
@_super()
|
||||
@set('selected', 'stylesheet')
|
||||
|
||||
headerActive: (->
|
||||
@get('selected') == 'header'
|
||||
).property('selected')
|
||||
|
||||
stylesheetActive: (->
|
||||
@get('selected') == 'stylesheet'
|
||||
).property('selected')
|
||||
|
||||
selectHeader: ->
|
||||
@set('selected', 'header')
|
||||
|
||||
selectStylesheet: ->
|
||||
@set('selected', 'stylesheet')
|
||||
|
||||
|
||||
didInsertElement: ->
|
||||
Mousetrap.bindGlobal ['meta+s', 'ctrl+s'], =>
|
||||
@get('controller').save()
|
||||
return false
|
||||
|
||||
willDestroyElement: ->
|
||||
Mousetrap.unbindGlobal('meta+s','ctrl+s')
|
||||
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
(function() {
|
||||
|
||||
Discourse.AdminDashboardView = window.Discourse.View.extend({
|
||||
templateName: 'admin/templates/dashboard'
|
||||
});
|
||||
|
||||
}).call(this);
|
|
@ -1,2 +0,0 @@
|
|||
Discourse.AdminDashboardView = window.Discourse.View.extend
|
||||
templateName: 'admin/templates/dashboard'
|
|
@ -0,0 +1,7 @@
|
|||
(function() {
|
||||
|
||||
Discourse.AdminEmailLogsView = window.Discourse.View.extend({
|
||||
templateName: 'admin/templates/email_logs'
|
||||
});
|
||||
|
||||
}).call(this);
|
|
@ -1,2 +0,0 @@
|
|||
Discourse.AdminEmailLogsView = window.Discourse.View.extend
|
||||
templateName: 'admin/templates/email_logs'
|
7
app/assets/javascripts/admin/views/admin_flags_view.js
Normal file
7
app/assets/javascripts/admin/views/admin_flags_view.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
(function() {
|
||||
|
||||
Discourse.AdminFlagsView = window.Discourse.View.extend({
|
||||
templateName: 'admin/templates/flags'
|
||||
});
|
||||
|
||||
}).call(this);
|
|
@ -1,3 +0,0 @@
|
|||
Discourse.AdminFlagsView = window.Discourse.View.extend
|
||||
templateName: 'admin/templates/flags'
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
(function() {
|
||||
|
||||
Discourse.AdminSiteSettingsView = window.Discourse.View.extend({
|
||||
templateName: 'admin/templates/site_settings'
|
||||
});
|
||||
|
||||
}).call(this);
|
|
@ -1,2 +0,0 @@
|
|||
Discourse.AdminSiteSettingsView = window.Discourse.View.extend
|
||||
templateName: 'admin/templates/site_settings'
|
7
app/assets/javascripts/admin/views/admin_user_view.js
Normal file
7
app/assets/javascripts/admin/views/admin_user_view.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
(function() {
|
||||
|
||||
Discourse.AdminUserView = window.Discourse.View.extend({
|
||||
templateName: 'admin/templates/user'
|
||||
});
|
||||
|
||||
}).call(this);
|
|
@ -1,2 +0,0 @@
|
|||
Discourse.AdminUserView = window.Discourse.View.extend
|
||||
templateName: 'admin/templates/user'
|
|
@ -0,0 +1,7 @@
|
|||
(function() {
|
||||
|
||||
Discourse.AdminUsersListView = window.Discourse.View.extend({
|
||||
templateName: 'admin/templates/users_list'
|
||||
});
|
||||
|
||||
}).call(this);
|
|
@ -1,2 +0,0 @@
|
|||
Discourse.AdminUsersListView = window.Discourse.View.extend
|
||||
templateName: 'admin/templates/users_list'
|
7
app/assets/javascripts/admin/views/admin_view.js
Normal file
7
app/assets/javascripts/admin/views/admin_view.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
(function() {
|
||||
|
||||
Discourse.AdminView = window.Discourse.View.extend({
|
||||
templateName: 'admin/templates/admin'
|
||||
});
|
||||
|
||||
}).call(this);
|
|
@ -1,2 +0,0 @@
|
|||
Discourse.AdminView = window.Discourse.View.extend
|
||||
templateName: 'admin/templates/admin'
|
|
@ -1,5 +1,5 @@
|
|||
// This is a manifest file that'll be compiled into including all the files listed below.
|
||||
// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
|
||||
// Add new JavaScript code in separate files in this directory and they'll automatically
|
||||
// be included in the compiled file accessible from http://example.com/assets/application.js
|
||||
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
|
||||
// the compiled file.
|
||||
|
|
|
@ -159,7 +159,7 @@ var EXTRA_PARENT_PATHS_RE = /^(?:\.\.\/)*(?:\.\.$)?/;
|
|||
* }
|
||||
*/
|
||||
function collapse_dots(path) {
|
||||
if (path === null) { return null; }
|
||||
if (path == null) { return null; }
|
||||
var p = normPath(path);
|
||||
// Only /../ left to flatten
|
||||
var r = PARENT_DIRECTORY_HANDLER_RE;
|
||||
|
@ -1875,7 +1875,7 @@ var html = (function(html4) {
|
|||
var parts = [];
|
||||
var lastPos = 0;
|
||||
var m;
|
||||
while ((m = re.exec(str)) !== null) {
|
||||
while ((m = re.exec(str)) != null) {
|
||||
parts.push(str.substring(lastPos, m.index));
|
||||
parts.push(m[0]);
|
||||
lastPos = m.index + m[0].length;
|
||||
|
@ -2085,7 +2085,7 @@ var html = (function(html4) {
|
|||
for (var i = 0, n = attribs.length; i < n; i += 2) {
|
||||
var attribName = attribs[i],
|
||||
value = attribs[i + 1];
|
||||
if (value !== null && value !== void 0) {
|
||||
if (value != null && value !== void 0) {
|
||||
out.push(' ', attribName, '="', escapeAttrib(value), '"');
|
||||
}
|
||||
}
|
||||
|
@ -2241,7 +2241,7 @@ var html = (function(html4) {
|
|||
html4.ATTRIBS.hasOwnProperty(attribKey))) {
|
||||
atype = html4.ATTRIBS[attribKey];
|
||||
}
|
||||
if (atype !== null) {
|
||||
if (atype != null) {
|
||||
switch (atype) {
|
||||
case html4.atype['NONE']: break;
|
||||
case html4.atype['SCRIPT']:
|
||||
|
@ -2318,7 +2318,7 @@ var html = (function(html4) {
|
|||
if (value && '#' === value.charAt(0)) {
|
||||
value = value.substring(1); // remove the leading '#'
|
||||
value = opt_nmTokenPolicy ? opt_nmTokenPolicy(value) : value;
|
||||
if (value !== null && value !== void 0) {
|
||||
if (value != null && value !== void 0) {
|
||||
value = '#' + value; // restore the leading '#'
|
||||
}
|
||||
} else {
|
||||
|
|
377
app/assets/javascripts/discourse.js
Normal file
377
app/assets/javascripts/discourse.js
Normal file
|
@ -0,0 +1,377 @@
|
|||
/*global Modernizr:true*/
|
||||
(function() {
|
||||
var csrf_token;
|
||||
|
||||
window.Discourse = Ember.Application.createWithMixins({
|
||||
rootElement: '#main',
|
||||
|
||||
// Data we want to remember for a short period
|
||||
transient: Em.Object.create(),
|
||||
|
||||
hasFocus: true,
|
||||
scrolling: false,
|
||||
|
||||
// The highest seen post number by topic
|
||||
highestSeenByTopic: {},
|
||||
|
||||
logoSmall: (function() {
|
||||
var logo;
|
||||
logo = Discourse.SiteSettings.logo_small_url;
|
||||
if (logo && logo.length > 1) {
|
||||
return "<img src='" + logo + "' width='33' height='33'>";
|
||||
} else {
|
||||
return "<i class='icon-home'></i>";
|
||||
}
|
||||
}).property(),
|
||||
|
||||
titleChanged: (function() {
|
||||
var title;
|
||||
title = "";
|
||||
if (this.get('title')) {
|
||||
title += "" + (this.get('title')) + " - ";
|
||||
}
|
||||
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() {
|
||||
var bus, user;
|
||||
bus = Discourse.MessageBus;
|
||||
|
||||
// We don't want to receive any previous user notidications
|
||||
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);
|
||||
});
|
||||
}
|
||||
return bus.subscribe("/notification", (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
|
||||
replaceState: function(path) {
|
||||
if (window.history &&
|
||||
window.history.pushState &&
|
||||
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);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
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);
|
||||
if (newTopicId = newMatches ? newMatches[2] : void 0) {
|
||||
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;
|
||||
} 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;
|
||||
}
|
||||
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";
|
||||
}
|
||||
};
|
||||
if (console && console.group) {
|
||||
console.group(f(name, data));
|
||||
} else {
|
||||
console.log("");
|
||||
console.log(f(name, data));
|
||||
}
|
||||
ary = [];
|
||||
_ref = window.probes;
|
||||
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;
|
||||
if (output = f("" + item.k, item.v)) {
|
||||
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
|
||||
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
|
||||
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();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
} 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({
|
||||
location: 'discourse_location'
|
||||
});
|
||||
|
||||
// since we have no jquery-rails these days, hook up csrf token
|
||||
csrf_token = jQuery('meta[name=csrf-token]').attr('content');
|
||||
|
||||
jQuery.ajaxPrefilter(function(options, originalOptions, xhr) {
|
||||
if (!options.crossDomain) {
|
||||
xhr.setRequestHeader('X-CSRF-Token', csrf_token);
|
||||
}
|
||||
});
|
||||
|
||||
}).call(this);
|
|
@ -1,270 +0,0 @@
|
|||
window.Discourse = Ember.Application.createWithMixins
|
||||
rootElement: '#main'
|
||||
|
||||
# Data we want to remember for a short period
|
||||
transient: Em.Object.create()
|
||||
|
||||
hasFocus: true
|
||||
scrolling: false
|
||||
|
||||
# The highest seen post number by topic
|
||||
highestSeenByTopic: {}
|
||||
|
||||
logoSmall: (->
|
||||
logo = Discourse.SiteSettings.logo_small_url
|
||||
if logo && logo.length > 1
|
||||
"<img src='#{logo}' width='33' height='33'>"
|
||||
else
|
||||
"<i class='icon-home'></i>"
|
||||
).property()
|
||||
|
||||
titleChanged: (->
|
||||
title = ""
|
||||
title += "#{@get('title')} - " if @get('title')
|
||||
title += Discourse.SiteSettings.title
|
||||
$('title').text(title)
|
||||
|
||||
title = ("(*) " + title) if !@get('hasFocus') && @get('notify')
|
||||
|
||||
# chrome bug workaround see: http://stackoverflow.com/questions/2952384/changing-the-window-title-when-focussing-the-window-doesnt-work-in-chrome
|
||||
window.setTimeout (->
|
||||
document.title = "."
|
||||
document.title = title
|
||||
return), 200
|
||||
return
|
||||
).observes('title', 'hasFocus', 'notify')
|
||||
|
||||
currentUserChanged: (->
|
||||
|
||||
bus = Discourse.MessageBus
|
||||
|
||||
# We don't want to receive any previous user notidications
|
||||
bus.unsubscribe "/notification"
|
||||
|
||||
bus.callbackInterval = Discourse.SiteSettings.anon_polling_interval
|
||||
bus.enableLongPolling = false
|
||||
|
||||
user = @get('currentUser')
|
||||
if user
|
||||
bus.callbackInterval = Discourse.SiteSettings.polling_interval
|
||||
bus.enableLongPolling = true
|
||||
|
||||
if user.admin
|
||||
bus.subscribe "/flagged_counts", (data) ->
|
||||
user.set('site_flagged_posts_count', data.total)
|
||||
bus.subscribe "/notification", ((data) ->
|
||||
user.set('unread_notifications', data.unread_notifications)
|
||||
user.set('unread_private_messages', data.unread_private_messages)), user.notification_channel_position
|
||||
|
||||
).observes('currentUser')
|
||||
|
||||
notifyTitle: ->
|
||||
@set('notify', true)
|
||||
|
||||
# Browser aware replaceState
|
||||
replaceState: (path) ->
|
||||
if window.history && window.history.pushState && window.history.replaceState && !navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]|WebApps\/.+CFNetwork)/)
|
||||
history.replaceState({path: path}, null, path) unless window.location.pathname is path
|
||||
|
||||
openComposer: (opts) ->
|
||||
# TODO, remove container link
|
||||
Discourse.__container__.lookup('controller:composer')?.open(opts)
|
||||
|
||||
# Like router.route, but allow full urls rather than relative ones
|
||||
# HERE BE HACKS - uses the ember container for now until we can do this nicer.
|
||||
routeTo: (path) ->
|
||||
path = path.replace(/https?\:\/\/[^\/]+/, '')
|
||||
|
||||
# If we're in the same topic, don't push the state
|
||||
topicRegexp = /\/t\/([^\/]+)\/(\d+)\/?(\d+)?/
|
||||
newMatches = topicRegexp.exec(path);
|
||||
if newTopicId = newMatches?[2]
|
||||
oldMatches = topicRegexp.exec(window.location.pathname);
|
||||
if (oldTopicId = oldMatches?[2]) && (oldTopicId is newTopicId)
|
||||
Discourse.replaceState(path)
|
||||
topicController = Discourse.__container__.lookup('controller:topic')
|
||||
opts = {trackVisit: false}
|
||||
opts.nearPost = newMatches[3] if 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)
|
||||
router.handleURL(path)
|
||||
|
||||
# Scroll to the top if we're not replacing state
|
||||
|
||||
|
||||
# The classes of buttons to show on a post
|
||||
postButtons: (->
|
||||
Discourse.SiteSettings.post_menu.split("|").map (i) -> "#{i.replace(/\+/, '').capitalize()}"
|
||||
).property('Discourse.SiteSettings.post_menu')
|
||||
|
||||
bindDOMEvents: ->
|
||||
|
||||
$html = $('html')
|
||||
# Add the discourse touch event
|
||||
hasTouch = false
|
||||
hasTouch = true if $html.hasClass('touch')
|
||||
hasTouch = true if (Modernizr.prefixed("MaxTouchPoints", navigator) > 1)
|
||||
|
||||
if hasTouch
|
||||
$html.addClass('discourse-touch')
|
||||
@touch = true
|
||||
@hasTouch = true
|
||||
else
|
||||
$html.addClass('discourse-no-touch')
|
||||
@touch = false
|
||||
|
||||
$('#main').on 'click.discourse', '[data-not-implemented=true]', (e) =>
|
||||
e.preventDefault()
|
||||
alert Em.String.i18n('not_implemented')
|
||||
false
|
||||
|
||||
$('#main').on 'click.discourse', 'a', (e) =>
|
||||
|
||||
return if (e.isDefaultPrevented() || e.metaKey || e.ctrlKey)
|
||||
$currentTarget = $(e.currentTarget)
|
||||
|
||||
href = $currentTarget.attr('href')
|
||||
return if href is undefined
|
||||
return if href is '#'
|
||||
return if $currentTarget.attr('target')
|
||||
return if $currentTarget.data('auto-route')
|
||||
return if $currentTarget.hasClass('lightbox')
|
||||
return if href.indexOf("mailto:") is 0
|
||||
|
||||
if href.match(/^http[s]?:\/\//i) && !href.match new RegExp("^http:\\/\\/" + window.location.hostname,"i")
|
||||
return
|
||||
|
||||
e.preventDefault()
|
||||
@routeTo(href)
|
||||
|
||||
false
|
||||
|
||||
$(window).focus( =>
|
||||
@set('hasFocus',true)
|
||||
@set('notify',false)
|
||||
).blur( =>
|
||||
@set('hasFocus',false)
|
||||
)
|
||||
|
||||
logout: ->
|
||||
username = @get('currentUser.username')
|
||||
Discourse.KeyValueStore.abandonLocal()
|
||||
$.ajax "/session/#{username}",
|
||||
type: 'DELETE'
|
||||
success: (result) =>
|
||||
# To keep lots of our variables unbound, we can handle a redirect on logging out.
|
||||
window.location.reload()
|
||||
|
||||
# fancy probes in ember
|
||||
insertProbes: ->
|
||||
|
||||
return unless console?
|
||||
|
||||
topLevel = (fn,name) ->
|
||||
window.probes.measure fn,
|
||||
name: name
|
||||
before: (data,owner, args) ->
|
||||
if owner
|
||||
window.probes.clear()
|
||||
|
||||
after: (data, owner, args) ->
|
||||
if owner && data.time > 10
|
||||
f = (name,data) ->
|
||||
"#{name} - #{data.count} calls #{(data.time + 0.0).toFixed(2)}ms" if data && data.count
|
||||
|
||||
if console && console.group
|
||||
console.group(f(name, data))
|
||||
else
|
||||
console.log("")
|
||||
console.log(f(name,data))
|
||||
|
||||
ary = []
|
||||
for n,v of window.probes
|
||||
continue if n == name || v.time < 1
|
||||
ary.push(k: n, v: v)
|
||||
|
||||
ary.sortBy((item) -> if item.v && item.v.time then -item.v.time else 0).each (item)->
|
||||
console.log output if output = f("#{item.k}", item.v)
|
||||
console?.groupEnd?()
|
||||
|
||||
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")
|
||||
return
|
||||
|
||||
authenticationComplete: (options)->
|
||||
# TODO, how to dispatch this to the view without the container?
|
||||
loginView = Discourse.__container__.lookup('controller:modal').get('currentView')
|
||||
loginView.authenticationComplete(options)
|
||||
|
||||
buildRoutes: (builder) ->
|
||||
oldBuilder = Discourse.routeBuilder
|
||||
Discourse.routeBuilder = ->
|
||||
oldBuilder.call(@) if oldBuilder
|
||||
builder.call(@)
|
||||
|
||||
start: ->
|
||||
@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 ->
|
||||
split = @href.split("/")
|
||||
id = split[split.length-1].split(".css")[0]
|
||||
stylesheet = @
|
||||
Discourse.MessageBus.subscribe "/file-change/#{id}", (data)=>
|
||||
$(stylesheet).data('orig', stylesheet.href) unless $(stylesheet).data('orig')
|
||||
orig = $(stylesheet).data('orig')
|
||||
sp = orig.split(".css?")
|
||||
stylesheet.href = sp[0] + ".css?" + data
|
||||
|
||||
$('header.custom').each ->
|
||||
header = $(this)
|
||||
Discourse.MessageBus.subscribe "/header-change/#{$(@).data('key')}", (data)->
|
||||
header.html(data)
|
||||
|
||||
# possibly move this to dev only
|
||||
Discourse.MessageBus.subscribe "/file-change", (data)->
|
||||
Ember.TEMPLATES["empty"] = Handlebars.compile("<div></div>")
|
||||
data.each (me)->
|
||||
if me == "refresh"
|
||||
document.location.reload(true)
|
||||
else if me.name.substr(-10) == "handlebars"
|
||||
js = me.name.replace(".handlebars","").replace("app/assets/javascripts","/assets")
|
||||
$LAB.script(js + "?hash=" + me.hash).wait ->
|
||||
templateName = js.replace(".js","").replace("/assets/","")
|
||||
$.each Ember.View.views, ->
|
||||
if(@get('templateName')==templateName)
|
||||
@set('templateName','empty')
|
||||
@rerender()
|
||||
Em.run.next =>
|
||||
@set('templateName', templateName)
|
||||
@rerender()
|
||||
else
|
||||
$('link').each ->
|
||||
if @href.match(me.name) and me.hash
|
||||
$(@).data('orig', @href) unless $(@).data('orig')
|
||||
@href = $(@).data('orig') + "&hash=" + me.hash
|
||||
|
||||
window.Discourse.Router = Discourse.Router.reopen(location: 'discourse_location')
|
||||
|
||||
# since we have no jquery-rails these days, hook up csrf token
|
||||
csrf_token = $('meta[name=csrf-token]').attr('content')
|
||||
|
||||
$.ajaxPrefilter (options,originalOptions,xhr) ->
|
||||
unless options.crossDomain
|
||||
xhr.setRequestHeader('X-CSRF-Token', csrf_token)
|
||||
return
|
||||
|
313
app/assets/javascripts/discourse/components/autocomplete.js
Normal file
313
app/assets/javascripts/discourse/components/autocomplete.js
Normal file
|
@ -0,0 +1,313 @@
|
|||
(function() {
|
||||
|
||||
(function($) {
|
||||
var template;
|
||||
template = null;
|
||||
$.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";
|
||||
inputSelectedItems = [];
|
||||
addInputSelectedItem = function(item) {
|
||||
var d, prev, transformed;
|
||||
if (options.transformComplete) {
|
||||
transformed = options.transformComplete(item);
|
||||
}
|
||||
d = jQuery("<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);
|
||||
jQuery(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);
|
||||
}
|
||||
return addInputSelectedItem(x);
|
||||
}
|
||||
});
|
||||
this.val("");
|
||||
completeStart = 0;
|
||||
wrap.click(function() {
|
||||
_this.focus();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
markSelected = function() {
|
||||
var links;
|
||||
links = div.find('li a');
|
||||
links.removeClass('selected');
|
||||
return jQuery(links[selectedOption]).addClass('selected');
|
||||
};
|
||||
renderAutocomplete = function() {
|
||||
var borderTop, mePos, pos, ul;
|
||||
if (div) {
|
||||
div.hide().remove();
|
||||
}
|
||||
if (autocompleteOptions.length === 0) {
|
||||
return;
|
||||
}
|
||||
div = jQuery(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) 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) {
|
||||
me.val("");
|
||||
addInputSelectedItem(term);
|
||||
} else {
|
||||
if (options.transformComplete) {
|
||||
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)) {
|
||||
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 jQuery(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) && 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;
|
||||
}
|
||||
}
|
||||
prevIsGood = /[a-zA-Z\.]/.test(prev);
|
||||
}
|
||||
}
|
||||
if (e.which === 27) {
|
||||
if (completeStart) {
|
||||
closeAutocomplete();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (completeStart) {
|
||||
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);
|
|
@ -1,257 +0,0 @@
|
|||
( ($) ->
|
||||
|
||||
template = null
|
||||
|
||||
$.fn.autocomplete = (options)->
|
||||
|
||||
return if @length == 0
|
||||
|
||||
if options && options.cancel && @data("closeAutocomplete")
|
||||
@data("closeAutocomplete")()
|
||||
return this
|
||||
|
||||
alert "only supporting one matcher at the moment" unless @length == 1
|
||||
|
||||
autocompleteOptions = null
|
||||
selectedOption = null
|
||||
completeStart = null
|
||||
completeEnd = null
|
||||
me = @
|
||||
div = null
|
||||
|
||||
# input is handled differently
|
||||
isInput = @[0].tagName == "INPUT"
|
||||
|
||||
inputSelectedItems = []
|
||||
addInputSelectedItem = (item) ->
|
||||
|
||||
transformed = options.transformComplete(item) if options.transformComplete
|
||||
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)
|
||||
|
||||
d.find('a').click ->
|
||||
closeAutocomplete()
|
||||
inputSelectedItems.splice($.inArray(item),1)
|
||||
$(this).parent().parent().remove()
|
||||
if options.onChangeItems
|
||||
options.onChangeItems(inputSelectedItems)
|
||||
|
||||
if isInput
|
||||
|
||||
width = @width()
|
||||
height = @height()
|
||||
|
||||
wrap = @wrap("<div class='ac-wrap clearfix'/>").parent()
|
||||
|
||||
wrap.width(width)
|
||||
|
||||
@width(80)
|
||||
@attr('name', @attr('name') + "-renamed")
|
||||
|
||||
vals = @val().split(",")
|
||||
|
||||
vals.each (x)->
|
||||
unless x == ""
|
||||
x = options.reverseTransform(x) if options.reverseTransform
|
||||
addInputSelectedItem(x)
|
||||
|
||||
@val("")
|
||||
completeStart = 0
|
||||
wrap.click =>
|
||||
@focus()
|
||||
true
|
||||
|
||||
|
||||
markSelected = ->
|
||||
links = div.find('li a')
|
||||
links.removeClass('selected')
|
||||
$(links[selectedOption]).addClass('selected')
|
||||
|
||||
renderAutocomplete = ->
|
||||
div.hide().remove() if div
|
||||
return if autocompleteOptions.length == 0
|
||||
div = $(options.template(options: autocompleteOptions))
|
||||
|
||||
ul = div.find('ul')
|
||||
selectedOption = 0
|
||||
markSelected()
|
||||
|
||||
ul.find('li').click ->
|
||||
selectedOption = ul.find('li').index(this)
|
||||
completeTerm(autocompleteOptions[selectedOption])
|
||||
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')) || 0
|
||||
div.css
|
||||
position: 'absolute',
|
||||
top: (mePos.top + pos.top - div.height() + borderTop) + 'px',
|
||||
left: (mePos.left + pos.left + 27) + 'px'
|
||||
|
||||
|
||||
updateAutoComplete = (r)->
|
||||
return if completeStart == null
|
||||
autocompleteOptions = r
|
||||
if !r || r.length == 0
|
||||
closeAutocomplete()
|
||||
else
|
||||
renderAutocomplete()
|
||||
|
||||
closeAutocomplete = ->
|
||||
div.hide().remove() if div
|
||||
div = null
|
||||
completeStart = null
|
||||
autocompleteOptions = null
|
||||
|
||||
# chain to allow multiples
|
||||
oldClose = me.data("closeAutocomplete")
|
||||
me.data "closeAutocomplete", ->
|
||||
oldClose() if oldClose
|
||||
closeAutocomplete()
|
||||
|
||||
completeTerm = (term) ->
|
||||
if term
|
||||
if isInput
|
||||
me.val("")
|
||||
addInputSelectedItem(term)
|
||||
else
|
||||
term = options.transformComplete(term) if options.transformComplete
|
||||
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)
|
||||
closeAutocomplete()
|
||||
|
||||
$(@).keypress (e) ->
|
||||
|
||||
|
||||
if !options.key
|
||||
return
|
||||
|
||||
# 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
|
||||
|
||||
$(@).keydown (e) ->
|
||||
|
||||
completeStart = 0 if !options.key
|
||||
|
||||
return if e.which == 16
|
||||
|
||||
if completeStart == null && e.which == 8 && options.key #backspace
|
||||
|
||||
c = Discourse.Utilities.caretPosition(me[0])
|
||||
next = me[0].value[c]
|
||||
nextIsGood = next == undefined || /\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
|
||||
|
||||
prevIsGood = /[a-zA-Z\.]/.test(prev)
|
||||
|
||||
|
||||
if e.which == 27 # esc key
|
||||
if completeStart != null
|
||||
closeAutocomplete()
|
||||
return false
|
||||
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
|
||||
when 13, 39, 9 # enter, tab or right arrow completes
|
||||
return true unless autocompleteOptions
|
||||
if selectedOption >= 0 and userToComplete = autocompleteOptions[selectedOption]
|
||||
completeTerm(userToComplete)
|
||||
else
|
||||
# We're cancelling it, really.
|
||||
return true
|
||||
|
||||
closeAutocomplete()
|
||||
return false
|
||||
when 38 # up arrow
|
||||
selectedOption = selectedOption - 1
|
||||
selectedOption = 0 if selectedOption < 0
|
||||
markSelected()
|
||||
return false
|
||||
when 40 # down arrow
|
||||
total = autocompleteOptions.length
|
||||
selectedOption = selectedOption + 1
|
||||
selectedOption = total - 1 if selectedOption >= total
|
||||
selectedOption = 0 if selectedOption < 0
|
||||
markSelected()
|
||||
return false
|
||||
else
|
||||
|
||||
# otherwise they're typing - let's search for it!
|
||||
completeEnd = caretPosition
|
||||
caretPosition-- if (e.which == 8)
|
||||
|
||||
if caretPosition < 0
|
||||
closeAutocomplete()
|
||||
if isInput
|
||||
i = wrap.find('a:last')
|
||||
i.click() if i
|
||||
|
||||
return false
|
||||
|
||||
term = me.val().substring(completeStart+(if options.key then 1 else 0), caretPosition)
|
||||
if (e.which > 48 && e.which < 90)
|
||||
term += String.fromCharCode(e.which)
|
||||
else
|
||||
term += "," unless e.which == 8 # backspace
|
||||
options.dataSource term, updateAutoComplete
|
||||
return true
|
||||
|
||||
|
||||
)(jQuery)
|
221
app/assets/javascripts/discourse/components/bbcode.js
Normal file
221
app/assets/javascripts/discourse/components/bbcode.js
Normal file
|
@ -0,0 +1,221 @@
|
|||
/*global HANDLEBARS_TEMPLATES:true*/
|
||||
|
||||
(function() {
|
||||
|
||||
Discourse.BBCode = {
|
||||
QUOTE_REGEXP: /\[quote=([^\]]*)\]([\s\S]*?)\[\/quote\]/im,
|
||||
/* Define our replacers
|
||||
*/
|
||||
|
||||
replacers: {
|
||||
base: {
|
||||
withoutArgs: {
|
||||
"ol": function(_, content) {
|
||||
return "<ol>" + content + "</ol>";
|
||||
},
|
||||
"li": function(_, content) {
|
||||
return "<li>" + content + "</li>";
|
||||
},
|
||||
"ul": function(_, content) {
|
||||
return "<ul>" + content + "</ul>";
|
||||
},
|
||||
"code": function(_, content) {
|
||||
return "<pre>" + content + "</pre>";
|
||||
},
|
||||
"url": function(_, url) {
|
||||
return "<a href=\"" + url + "\">" + url + "</a>";
|
||||
},
|
||||
"email": function(_, address) {
|
||||
return "<a href=\"mailto:" + address + "\">" + address + "</a>";
|
||||
},
|
||||
"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>";
|
||||
}
|
||||
}
|
||||
},
|
||||
/* 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>";
|
||||
}
|
||||
},
|
||||
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";
|
||||
},
|
||||
|
||||
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);
|
|
@ -1,130 +0,0 @@
|
|||
Discourse.BBCode =
|
||||
|
||||
QUOTE_REGEXP: /\[quote=([^\]]*)\]([\s\S]*?)\[\/quote\]/im
|
||||
|
||||
# Define our replacers
|
||||
replacers:
|
||||
|
||||
base:
|
||||
withoutArgs:
|
||||
"ol": (_, content) -> "<ol>#{content}</ol>"
|
||||
"li": (_, content) -> "<li>#{content}</li>"
|
||||
"ul": (_, content) -> "<ul>#{content}</ul>"
|
||||
"code": (_, content) -> "<pre>#{content}</pre>"
|
||||
"url": (_, url) -> "<a href=\"#{url}\">#{url}</a>"
|
||||
"email": (_, address) -> "<a href=\"mailto:#{address}\">#{address}</a>"
|
||||
"img": (_, src) -> "<img src=\"#{src}\">"
|
||||
withArgs:
|
||||
"url": (_, href, title) -> "<a href=\"#{href}\">#{title}</a>"
|
||||
"email": (_, address, title) -> "<a href=\"mailto:#{address}\">#{title}</a>"
|
||||
"color": (_, color, content) ->
|
||||
return content unless /^(\#[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)
|
||||
"<span style=\"color: #{color}\">#{content}</span>"
|
||||
|
||||
# For HTML emails
|
||||
email:
|
||||
withoutArgs:
|
||||
"b": (_, content) -> "<b>#{content}</b>"
|
||||
"i": (_, content) -> "<i>#{content}</i>"
|
||||
"u": (_, content) -> "<u>#{content}</u>"
|
||||
"s": (_, content) -> "<s>#{content}</s>"
|
||||
"spoiler": (_, content) -> "<span style='background-color: #000'>#{content}</span>"
|
||||
|
||||
withArgs:
|
||||
"size": (_, size, content) -> "<span style=\"font-size: #{size}px\">#{content}</span>"
|
||||
|
||||
# For sane environments that support CSS
|
||||
default:
|
||||
withoutArgs:
|
||||
"b": (_, content) -> "<span class='bbcode-b'>#{content}</span>"
|
||||
"i": (_, content) -> "<span class='bbcode-i'>#{content}</span>"
|
||||
"u": (_, content) -> "<span class='bbcode-u'>#{content}</span>"
|
||||
"s": (_, content) -> "<span class='bbcode-s'>#{content}</span>"
|
||||
"spoiler": (_, content) -> "<span class=\"spoiler\">#{content}</span>"
|
||||
|
||||
withArgs:
|
||||
"size": (_, size, content) -> "<span class=\"bbcode-size-#{size}\">#{content}</span>"
|
||||
|
||||
# Apply a particular set of replacers
|
||||
apply: (text, environment) ->
|
||||
replacer = Discourse.BBCode.parsedReplacers()[environment]
|
||||
replacer.forEach (r) -> text = text.replace r.regexp, r.fn
|
||||
text
|
||||
|
||||
parsedReplacers: ->
|
||||
return @parsed if @parsed
|
||||
result = {}
|
||||
|
||||
Object.keys Discourse.BBCode.replacers, (name, rules) ->
|
||||
parsed = result[name] = []
|
||||
|
||||
Object.keys Object.merge(Discourse.BBCode.replacers.base.withoutArgs, rules.withoutArgs), (tag, val) ->
|
||||
parsed.push(regexp: RegExp("\\[#{tag}\\]([\\s\\S]*?)\\[\\/#{tag}\\]", "igm"), fn: val)
|
||||
|
||||
Object.keys Object.merge(Discourse.BBCode.replacers.base.withArgs, rules.withArgs), (tag, val) ->
|
||||
parsed.push(regexp: RegExp("\\[#{tag}=?(.+?)\\\]([\\s\\S]*?)\\[\\/#{tag}\\]", "igm"), fn: val)
|
||||
|
||||
@parsed = result
|
||||
@parsed
|
||||
|
||||
buildQuoteBBCode: (post, contents="") ->
|
||||
sansQuotes = contents.replace(@QUOTE_REGEXP, '').trim()
|
||||
return "" if sansQuotes.length == 0
|
||||
|
||||
# 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"
|
||||
|
||||
formatQuote: (text, opts) ->
|
||||
|
||||
# Replace quotes with appropriate markup
|
||||
while matches = @QUOTE_REGEXP.exec(text)
|
||||
paramsString = matches[1]
|
||||
paramsString = paramsString.replace(/\"/g, '')
|
||||
paramsSplit = paramsString.split(/\, */)
|
||||
|
||||
params=[]
|
||||
paramsSplit.each (p, i) ->
|
||||
if i > 0
|
||||
assignment = p.split(':')
|
||||
if assignment[0] and assignment[1]
|
||||
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(username) if opts.lookupAvatar
|
||||
|
||||
templateName = 'quote'
|
||||
templateName = "quote_#{opts.environment}" if opts?.environment
|
||||
|
||||
text = text.replace(matches[0], "</p>" + HANDLEBARS_TEMPLATES[templateName](args) + "<p>")
|
||||
|
||||
text
|
||||
|
||||
format: (text, opts) ->
|
||||
text = Discourse.BBCode.apply(text, opts?.environment || 'default')
|
||||
|
||||
# Add quotes
|
||||
text = Discourse.BBCode.formatQuote(text, opts)
|
||||
|
||||
text
|
135
app/assets/javascripts/discourse/components/caret_position.js
Normal file
135
app/assets/javascripts/discourse/components/caret_position.js
Normal file
|
@ -0,0 +1,135 @@
|
|||
|
||||
/* caret position in textarea ... very hacky ... sorry
|
||||
*/
|
||||
|
||||
|
||||
(function() {
|
||||
|
||||
(function($) {
|
||||
/* 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;
|
||||
};
|
||||
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 : 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, "<");
|
||||
buf = buf.replace(/>/g, ">");
|
||||
buf = buf.replace(/[ ]/g, "​ ​");
|
||||
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,
|
||||
top: (pos.top - pPos.top) - clone.scrollTop()
|
||||
};
|
||||
};
|
||||
return $.fn.caretPosition;
|
||||
|
||||
})(jQuery);
|
||||
|
||||
}).call(this);
|
|
@ -1,101 +0,0 @@
|
|||
# caret position in textarea ... very hacky ... sorry
|
||||
(($) ->
|
||||
|
||||
# http://stackoverflow.com/questions/263743/how-to-get-caret-position-in-textarea
|
||||
getCaret = (el) ->
|
||||
if el.selectionStart
|
||||
return el.selectionStart
|
||||
else if document.selection
|
||||
el.focus()
|
||||
r = document.selection.createRange()
|
||||
return 0 if r is null
|
||||
re = el.createTextRange()
|
||||
rc = re.duplicate()
|
||||
re.moveToBookmark r.getBookmark()
|
||||
rc.setEndPoint "EndToStart", re
|
||||
return rc.text.length
|
||||
0
|
||||
|
||||
clone = null
|
||||
$.fn.caretPosition = (options) ->
|
||||
|
||||
clone.remove() if clone
|
||||
span = $("#pos span")
|
||||
textarea = $(this)
|
||||
getStyles = (el, prop) ->
|
||||
if el.currentStyle
|
||||
el.currentStyle
|
||||
else
|
||||
document.defaultView.getComputedStyle el, ""
|
||||
|
||||
styles = getStyles(textarea[0])
|
||||
clone = $("<div><p></p></div>").appendTo("body")
|
||||
p = clone.find("p")
|
||||
clone.width textarea.width()
|
||||
clone.height textarea.height()
|
||||
|
||||
important = (prop) ->
|
||||
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 = undefined
|
||||
after = undefined
|
||||
pos = if options && options.pos then options.pos else 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
|
||||
insertSpaceAfterBefore = true if val[before] is "\n" and val[after] is "\n"
|
||||
guard = (v) ->
|
||||
buf = v.replace(/</g,"<")
|
||||
buf = buf.replace(/>/g,">")
|
||||
buf = buf.replace(/[ ]/g, "​ ​")
|
||||
buf.replace(/\n/g,"<br />")
|
||||
|
||||
|
||||
makeCursor = (pos, klass, color) ->
|
||||
l = val.substring(pos, pos + 1)
|
||||
return "<br>" if l is "\n"
|
||||
"<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")
|
||||
html += makeCursor(0, "post-before", "#d0ffff") if insertSpaceAfterBefore
|
||||
if after >= 0
|
||||
html += makeCursor(after, "after", "#ffd0ff")
|
||||
html += guard(val.substring(after + 1)) if after - 1 < val.length
|
||||
p.html html
|
||||
clone.scrollTop textarea.scrollTop()
|
||||
letter = p.find("span:first")
|
||||
pos = letter.offset()
|
||||
pos.left = pos.left + letter.width() if letter.hasClass("before")
|
||||
pPos = p.offset()
|
||||
#clone.hide().remove()
|
||||
|
||||
left: pos.left - pPos.left
|
||||
top: (pos.top - pPos.top) - clone.scrollTop()
|
||||
) jQuery
|
108
app/assets/javascripts/discourse/components/click_track.js
Normal file
108
app/assets/javascripts/discourse/components/click_track.js
Normal file
|
@ -0,0 +1,108 @@
|
|||
|
||||
/* We use this object to keep track of click counts.
|
||||
*/
|
||||
|
||||
|
||||
(function() {
|
||||
|
||||
window.Discourse.ClickTrack = {
|
||||
/* Pass the event of the click here and we'll do the magic!
|
||||
*/
|
||||
|
||||
trackClick: function(e) {
|
||||
var $a, $article, $badge, count, destination, href, ownLink, postId, topicId, trackingUrl, userId;
|
||||
$a = jQuery(e.currentTarget);
|
||||
if ($a.hasClass('lightbox')) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
/* We don't track clicks on quote back buttons
|
||||
*/
|
||||
|
||||
if ($a.hasClass('back') || $a.hasClass('quote-other-topic')) {
|
||||
return true;
|
||||
}
|
||||
/* Remove the href, put it as a data attribute
|
||||
*/
|
||||
|
||||
if (!$a.data('href')) {
|
||||
$a.addClass('no-href');
|
||||
$a.data('href', $a.attr('href'));
|
||||
$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 = 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;
|
||||
}
|
||||
};
|
||||
|
||||
}).call(this);
|
|
@ -1,66 +0,0 @@
|
|||
# We use this object to keep track of click counts.
|
||||
window.Discourse.ClickTrack =
|
||||
|
||||
# Pass the event of the click here and we'll do the magic!
|
||||
trackClick: (e) ->
|
||||
|
||||
$a = $(e.currentTarget)
|
||||
|
||||
return if $a.hasClass('lightbox')
|
||||
|
||||
e.preventDefault()
|
||||
|
||||
# We don't track clicks on quote back buttons
|
||||
return true if $a.hasClass('back') or $a.hasClass('quote-other-topic')
|
||||
|
||||
# Remove the href, put it as a data attribute
|
||||
unless $a.data('href')
|
||||
$a.addClass('no-href')
|
||||
$a.data('href', $a.attr('href'))
|
||||
$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')
|
||||
userId = $article.data('user-id') unless userId
|
||||
|
||||
ownLink = userId and (userId is Discourse.get('currentUser.id'))
|
||||
|
||||
# Build a Redirect URL
|
||||
trackingUrl = "/clicks/track?url=" + encodeURIComponent(href)
|
||||
trackingUrl += "&post_id=" + encodeURI(postId) if postId and (not $a.data('ignore-post-id'))
|
||||
trackingUrl += "&topic_id=" + encodeURI(topicId) if topicId
|
||||
|
||||
# Update badge clicks unless it's our own
|
||||
unless ownLink
|
||||
$badge = $('span.badge', $a)
|
||||
if $badge.length == 1
|
||||
count = parseInt($badge.html())
|
||||
$badge.html(count + 1)
|
||||
|
||||
# If they right clicked, change the destination href
|
||||
if e.which is 3
|
||||
destination = if Discourse.SiteSettings.track_external_right_clicks then trackingUrl else 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 is 2)
|
||||
$.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
|
||||
$.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
|
||||
false
|
33
app/assets/javascripts/discourse/components/debounce.js
Normal file
33
app/assets/javascripts/discourse/components/debounce.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
(function() {
|
||||
|
||||
window.Discourse.debounce = function(func, wait, trickle) {
|
||||
var timeout;
|
||||
timeout = null;
|
||||
return function() {
|
||||
var args, context, currentWait, later;
|
||||
context = this;
|
||||
args = arguments;
|
||||
later = function() {
|
||||
timeout = null;
|
||||
return func.apply(context, args);
|
||||
};
|
||||
if (timeout && trickle) {
|
||||
/* already queued, let it through
|
||||
*/
|
||||
|
||||
return;
|
||||
}
|
||||
if (typeof wait === "function") {
|
||||
currentWait = wait();
|
||||
} else {
|
||||
currentWait = wait;
|
||||
}
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
timeout = setTimeout(later, currentWait);
|
||||
return timeout;
|
||||
};
|
||||
};
|
||||
|
||||
}).call(this);
|
|
@ -1,20 +0,0 @@
|
|||
window.Discourse.debounce = (func, wait, trickle) ->
|
||||
timeout = null
|
||||
return ->
|
||||
context = @
|
||||
args = arguments
|
||||
later = ->
|
||||
timeout = null
|
||||
func.apply(context, args)
|
||||
|
||||
if timeout != null && trickle
|
||||
# already queued, let it through
|
||||
return
|
||||
|
||||
if typeof wait == "function"
|
||||
currentWait = wait()
|
||||
else
|
||||
currentWait = wait
|
||||
|
||||
clearTimeout(timeout) if timeout
|
||||
timeout = setTimeout(later, currentWait)
|
|
@ -0,0 +1,10 @@
|
|||
(function() {
|
||||
|
||||
Discourse.TextField = Ember.TextField.extend({
|
||||
attributeBindings: ['autocorrect', 'autocapitalize'],
|
||||
placeholder: (function() {
|
||||
return Em.String.i18n(this.get('placeholderKey'));
|
||||
}).property('placeholderKey')
|
||||
});
|
||||
|
||||
}).call(this);
|
|
@ -1,7 +0,0 @@
|
|||
Discourse.TextField = Ember.TextField.extend
|
||||
|
||||
attributeBindings: ['autocorrect', 'autocapitalize']
|
||||
|
||||
placeholder: (->
|
||||
Em.String.i18n(@get('placeholderKey'))
|
||||
).property('placeholderKey')
|
92
app/assets/javascripts/discourse/components/div_resizer.js
Normal file
92
app/assets/javascripts/discourse/components/div_resizer.js
Normal file
|
@ -0,0 +1,92 @@
|
|||
|
||||
/*based off text area resizer by Ryan O'Dell : http://plugins.jquery.com/misc/textarea.js
|
||||
*/
|
||||
|
||||
|
||||
(function() {
|
||||
|
||||
(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;
|
||||
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;
|
||||
thisMousePos = mousePosition(e).y;
|
||||
size = originalDivHeight + (originalPos - thisMousePos);
|
||||
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);
|
||||
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 = 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);
|
|
@ -1,65 +0,0 @@
|
|||
#based off text area resizer by Ryan O'Dell : http://plugins.jquery.com/misc/textarea.js
|
||||
(($) ->
|
||||
|
||||
div = undefined
|
||||
originalPos = undefined
|
||||
originalDivHeight = undefined
|
||||
lastMousePos = 0
|
||||
min = 230
|
||||
grip = undefined
|
||||
wrappedEndDrag = undefined
|
||||
wrappedPerformDrag = undefined
|
||||
|
||||
startDrag = (e,opts) ->
|
||||
div = $(e.data.el)
|
||||
div.addClass('clear-transitions')
|
||||
div.blur()
|
||||
lastMousePos = mousePosition(e).y
|
||||
originalPos = lastMousePos
|
||||
originalDivHeight = div.height()
|
||||
wrappedPerformDrag = ( ->
|
||||
(e) -> performDrag(e,opts)
|
||||
)()
|
||||
wrappedEndDrag = ( ->
|
||||
(e) -> endDrag(e,opts)
|
||||
)()
|
||||
$(document).mousemove(wrappedPerformDrag).mouseup wrappedEndDrag
|
||||
false
|
||||
performDrag = (e,opts) ->
|
||||
thisMousePos = mousePosition(e).y
|
||||
size = originalDivHeight + (originalPos - thisMousePos)
|
||||
lastMousePos = thisMousePos
|
||||
size = Math.min(size, $(window).height())
|
||||
size = Math.max(min, size)
|
||||
|
||||
sizePx = size + "px"
|
||||
opts.onDrag?(sizePx)
|
||||
div.height(sizePx)
|
||||
endDrag e,opts if size < min
|
||||
false
|
||||
endDrag = (e,opts) ->
|
||||
$(document).unbind("mousemove", wrappedPerformDrag).unbind "mouseup", wrappedEndDrag
|
||||
div.removeClass('clear-transitions')
|
||||
div.focus()
|
||||
opts.resize?()
|
||||
div = null
|
||||
mousePosition = (e) ->
|
||||
x: e.clientX + document.documentElement.scrollLeft
|
||||
y: e.clientY + document.documentElement.scrollTop
|
||||
|
||||
$.fn.DivResizer = (opts) ->
|
||||
@each ->
|
||||
div = $(this)
|
||||
return if (div.hasClass("processed"))
|
||||
|
||||
div.addClass("processed")
|
||||
staticOffset = null
|
||||
|
||||
start = ->
|
||||
(e) -> startDrag(e,opts)
|
||||
|
||||
grippie = div.prepend("<div class='grippie'></div>").find('.grippie').bind("mousedown",
|
||||
el: this
|
||||
, start())
|
||||
) jQuery
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
#
|
||||
# Track visible elements on the screen
|
||||
#
|
||||
# You can register for triggers on:
|
||||
# focusChanged: -> the top element we're focusing on
|
||||
# seenElement: -> if we've seen the element
|
||||
#
|
||||
class Discourse.Eyeline
|
||||
|
||||
constructor: (@selector) ->
|
||||
|
||||
# Call this whenever we want to consider what is currently being seen by the browser
|
||||
update: ->
|
||||
docViewTop = $(window).scrollTop()
|
||||
windowHeight = $(window).height()
|
||||
docViewBottom = docViewTop + windowHeight
|
||||
documentHeight = $(document).height()
|
||||
|
||||
$elements = $(@selector)
|
||||
|
||||
atBottom = false
|
||||
if bottomOffset = $elements.last().offset()
|
||||
atBottom = (bottomOffset.top <= docViewBottom) and (bottomOffset.top >= docViewTop)
|
||||
|
||||
# Whether we've seen any elements in this search
|
||||
foundElement = false
|
||||
|
||||
$results = $(@selector)
|
||||
$results.each (i, elem) =>
|
||||
$elem = $(elem)
|
||||
|
||||
elemTop = $elem.offset().top
|
||||
elemBottom = elemTop + $elem.height()
|
||||
|
||||
markSeen = false
|
||||
|
||||
# It's seen if...
|
||||
# ...the element is vertically within the top and botom
|
||||
markSeen = true if ((elemTop <= docViewBottom) and (elemTop >= docViewTop))
|
||||
# ...the element top is above the top and the bottom is below the bottom (large elements)
|
||||
markSeen = true if ((elemTop <= docViewTop) and (elemBottom >= docViewBottom))
|
||||
# ...we're at the bottom and the bottom of the element is visible (large bottom elements)
|
||||
markSeen = true if atBottom and (elemBottom >= docViewTop)
|
||||
|
||||
return true unless markSeen
|
||||
|
||||
# If you hit the bottom we mark all the elements as seen. Otherwise, just the first one
|
||||
unless atBottom
|
||||
@trigger('saw', detail: $elem)
|
||||
@trigger('sawTop', detail: $elem) if i == 0
|
||||
return false
|
||||
|
||||
@trigger('sawTop', detail: $elem) if i == 0
|
||||
@trigger('sawBottom', detail: $elem) if i == ($results.length - 1)
|
||||
|
||||
# Call this when we know aren't loading any more elements. Mark the rest
|
||||
# as seen
|
||||
flushRest: ->
|
||||
$(@selector).each (i, elem) =>
|
||||
$elem = $(elem)
|
||||
@trigger('saw', detail: $elem)
|
||||
|
||||
|
||||
RSVP.EventTarget.mixin(Discourse.Eyeline.prototype)
|
129
app/assets/javascripts/discourse/components/eyeline.js
Normal file
129
app/assets/javascripts/discourse/components/eyeline.js
Normal file
|
@ -0,0 +1,129 @@
|
|||
|
||||
/* Track visible elements on the screen
|
||||
*/
|
||||
|
||||
|
||||
/* You can register for triggers on:
|
||||
*/
|
||||
|
||||
|
||||
/* focusChanged: -> the top element we're focusing on
|
||||
*/
|
||||
|
||||
|
||||
/* seenElement: -> if we've seen the element
|
||||
*/
|
||||
|
||||
|
||||
(function() {
|
||||
|
||||
Discourse.Eyeline = (function() {
|
||||
|
||||
function Eyeline(selector) {
|
||||
this.selector = selector;
|
||||
}
|
||||
|
||||
/* Call this whenever we want to consider what is currently being seen by the browser
|
||||
*/
|
||||
|
||||
|
||||
Eyeline.prototype.update = function() {
|
||||
var $elements, $results, atBottom, bottomOffset, docViewBottom, docViewTop, documentHeight, foundElement, windowHeight,
|
||||
_this = this;
|
||||
docViewTop = jQuery(window).scrollTop();
|
||||
windowHeight = jQuery(window).height();
|
||||
docViewBottom = docViewTop + windowHeight;
|
||||
documentHeight = jQuery(document).height();
|
||||
$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
|
||||
*/
|
||||
|
||||
foundElement = false;
|
||||
$results = jQuery(this.selector);
|
||||
return $results.each(function(i, elem) {
|
||||
var $elem, elemBottom, elemTop, markSeen;
|
||||
$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
|
||||
*/
|
||||
|
||||
|
||||
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);
|
|
@ -1,33 +0,0 @@
|
|||
# key value store
|
||||
#
|
||||
|
||||
window.Discourse.KeyValueStore = (->
|
||||
initialized = false
|
||||
context = ""
|
||||
|
||||
init: (ctx,messageBus) ->
|
||||
initialized = true
|
||||
context = ctx
|
||||
|
||||
abandonLocal: ->
|
||||
return unless localStorage && initialized
|
||||
i=localStorage.length-1
|
||||
while i >= 0
|
||||
k = localStorage.key(i)
|
||||
localStorage.removeItem(k) if k.substring(0, context.length) == context
|
||||
i--
|
||||
return true
|
||||
|
||||
remove: (key)->
|
||||
localStorage.removeItem(context + key)
|
||||
|
||||
set: (opts)->
|
||||
return false unless localStorage && initialized
|
||||
localStorage[context + opts["key"]] = opts["value"]
|
||||
|
||||
|
||||
get: (key)->
|
||||
return null unless localStorage
|
||||
localStorage[context + key]
|
||||
)()
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
|
||||
/* key value store
|
||||
*/
|
||||
|
||||
|
||||
(function() {
|
||||
|
||||
window.Discourse.KeyValueStore = (function() {
|
||||
var context, initialized;
|
||||
initialized = false;
|
||||
context = "";
|
||||
return {
|
||||
init: function(ctx, messageBus) {
|
||||
initialized = true;
|
||||
context = ctx;
|
||||
},
|
||||
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];
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
}).call(this);
|
23
app/assets/javascripts/discourse/components/lightbox.js
Normal file
23
app/assets/javascripts/discourse/components/lightbox.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
|
||||
/* Helper object for light boxes. Uses highlight.js which is loaded
|
||||
*/
|
||||
|
||||
|
||||
/* on demand.
|
||||
*/
|
||||
|
||||
|
||||
(function() {
|
||||
|
||||
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);
|
|
@ -1,8 +0,0 @@
|
|||
# Helper object for light boxes. Uses highlight.js which is loaded
|
||||
# on demand.
|
||||
window.Discourse.Lightbox =
|
||||
|
||||
apply: ($elem) ->
|
||||
$('a.lightbox', $elem).each (i, e) =>
|
||||
$LAB.script("/javascripts/jquery.colorbox-min.js").wait ->
|
||||
$(e).colorbox()
|
158
app/assets/javascripts/discourse/components/message_bus.js
Normal file
158
app/assets/javascripts/discourse/components/message_bus.js
Normal file
|
@ -0,0 +1,158 @@
|
|||
(function() {
|
||||
|
||||
window.Discourse.MessageBus = (function() {
|
||||
/* http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript
|
||||
*/
|
||||
|
||||
var callbacks, clientId, failCount, interval, isHidden, queue, responseCallbacks, uniqueId;
|
||||
uniqueId = function() {
|
||||
return 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||
var r, v;
|
||||
r = Math.random() * 16 | 0;
|
||||
v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
};
|
||||
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');
|
||||
}
|
||||
};
|
||||
return {
|
||||
enableLongPolling: true,
|
||||
callbackInterval: 60000,
|
||||
maxPollInterval: 3 * 60 * 1000,
|
||||
callbacks: callbacks,
|
||||
clientId: clientId,
|
||||
/*TODO
|
||||
*/
|
||||
|
||||
stop: false,
|
||||
/* Start polling
|
||||
*/
|
||||
|
||||
start: function(opts) {
|
||||
var poll,
|
||||
_this = this;
|
||||
if (!opts) opts = {};
|
||||
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];
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
error: failCount += 1,
|
||||
complete: function() {
|
||||
if (gotData) {
|
||||
setTimeout(poll, 100);
|
||||
} else {
|
||||
interval = _this.callbackInterval;
|
||||
if (failCount > 2) {
|
||||
interval = interval * failCount;
|
||||
} else if (isHidden()) {
|
||||
/* slowning down stuff a lot when hidden
|
||||
*/
|
||||
|
||||
/* we will need to add a lot of fine tuning here
|
||||
*/
|
||||
|
||||
interval = interval * 4;
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
}).call(this);
|
|
@ -1,114 +0,0 @@
|
|||
window.Discourse.MessageBus = ( ->
|
||||
|
||||
# http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript
|
||||
uniqueId = -> 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace /[xy]/g, (c)->
|
||||
r = Math.random()*16 | 0
|
||||
v = if c == 'x' then r else (r&0x3|0x8)
|
||||
v.toString(16)
|
||||
|
||||
clientId = uniqueId()
|
||||
|
||||
responseCallbacks = {}
|
||||
callbacks = []
|
||||
queue = []
|
||||
interval = null
|
||||
|
||||
failCount = 0
|
||||
|
||||
isHidden = ->
|
||||
if document.hidden != undefined
|
||||
document.hidden
|
||||
else if document.webkitHidden != undefined
|
||||
document.webkitHidden
|
||||
else if document.msHidden != undefined
|
||||
document.msHidden
|
||||
else if document.mozHidden != undefined
|
||||
document.mozHidden
|
||||
else
|
||||
# fallback to problamatic window.focus
|
||||
!Discourse.get('hasFocus')
|
||||
|
||||
enableLongPolling: true
|
||||
callbackInterval: 60000
|
||||
maxPollInterval: (3 * 60 * 1000)
|
||||
callbacks: callbacks
|
||||
clientId: clientId
|
||||
|
||||
#TODO
|
||||
stop:
|
||||
false
|
||||
|
||||
# Start polling
|
||||
start: (opts={})->
|
||||
|
||||
poll = =>
|
||||
if callbacks.length == 0
|
||||
setTimeout poll, 500
|
||||
return
|
||||
|
||||
data = {}
|
||||
callbacks.each (c)->
|
||||
data[c.channel] = if c.last_id == undefined then -1 else c.last_id
|
||||
|
||||
gotData = false
|
||||
|
||||
@longPoll = $.ajax "/message-bus/#{clientId}/poll?#{if isHidden() || !@enableLongPolling then "dlp=t" else ""}",
|
||||
data: data
|
||||
cache: false
|
||||
dataType: 'json'
|
||||
type: 'POST'
|
||||
headers:
|
||||
'X-SILENCE-LOGGER': 'true'
|
||||
success: (messages) =>
|
||||
failCount = 0
|
||||
messages.each (message) =>
|
||||
gotData = true
|
||||
callbacks.each (callback) ->
|
||||
if callback.channel == message.channel
|
||||
callback.last_id = message.message_id
|
||||
callback.func(message.data)
|
||||
if message["channel"] == "/__status"
|
||||
callback.last_id = message.data[callback.channel] if message.data[callback.channel] != undefined
|
||||
return
|
||||
error:
|
||||
failCount += 1
|
||||
complete: =>
|
||||
if gotData
|
||||
setTimeout poll, 100
|
||||
else
|
||||
interval = @callbackInterval
|
||||
if failCount > 2
|
||||
interval = interval * failCount
|
||||
else if isHidden()
|
||||
# slowning down stuff a lot when hidden
|
||||
# we will need to add a lot of fine tuning here
|
||||
interval = interval * 4
|
||||
|
||||
if interval > @maxPollInterval
|
||||
interval = @maxPollInterval
|
||||
|
||||
setTimeout poll, interval
|
||||
@longPoll = null
|
||||
return
|
||||
|
||||
poll()
|
||||
return
|
||||
|
||||
# Subscribe to a channel
|
||||
subscribe: (channel,func,lastId)->
|
||||
callbacks.push {channel:channel, func:func, last_id: lastId}
|
||||
@longPoll.abort() if @longPoll
|
||||
|
||||
# Unsubscribe from a channel
|
||||
unsubscribe: (channel) ->
|
||||
# TODO proper globbing
|
||||
if channel.endsWith("*")
|
||||
channel = channel.substr(0, channel.length-1)
|
||||
glob = true
|
||||
callbacks = callbacks.filter (callback) ->
|
||||
if glob
|
||||
callback.channel.substr(0, channel.length) != channel
|
||||
else
|
||||
callback.channel != channel
|
||||
@longPoll.abort() if @longPoll
|
||||
)()
|
|
@ -0,0 +1,38 @@
|
|||
/*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);
|
|
@ -1,24 +0,0 @@
|
|||
window.Discourse.PagedownEditor = Ember.ContainerView.extend
|
||||
elementId: 'pagedown-editor'
|
||||
|
||||
init: ->
|
||||
|
||||
@_super()
|
||||
|
||||
# Add a button bar
|
||||
@pushObject Em.View.create(elementId: 'wmd-button-bar')
|
||||
@pushObject Em.TextArea.create(valueBinding: 'parentView.value', elementId: 'wmd-input')
|
||||
@pushObject Em.View.createWithMixins Discourse.Presence,
|
||||
elementId: 'wmd-preview',
|
||||
classNameBindings: [':preview', 'hidden']
|
||||
|
||||
hidden: (->
|
||||
@blank('parentView.value')
|
||||
).property('parentView.value')
|
||||
|
||||
|
||||
didInsertElement: ->
|
||||
$wmdInput = $('#wmd-input')
|
||||
$wmdInput.data('init', true)
|
||||
@editor = new Markdown.Editor(Discourse.Utilities.markdownConverter(sanitize: true))
|
||||
@editor.run()
|
|
@ -51,19 +51,19 @@ someFunction = window.probes.measure(someFunction, "someFunction");
|
|||
}
|
||||
else
|
||||
{
|
||||
nameParam = options["name"];
|
||||
nameParam = options.name;
|
||||
|
||||
if (nameParam === "measure" || nameParam == "clear") {
|
||||
throw Error("can not be called measure or clear");
|
||||
if (nameParam === "measure" || nameParam === "clear") {
|
||||
throw new Error("can not be called measure or clear");
|
||||
}
|
||||
|
||||
if (!nameParam)
|
||||
{
|
||||
throw Error("you must specify the name option measure(fn, {name: 'some name'})");
|
||||
throw new Error("you must specify the name option measure(fn, {name: 'some name'})");
|
||||
}
|
||||
|
||||
before = options["before"];
|
||||
after = options["after"];
|
||||
before = options.before;
|
||||
after = options.after;
|
||||
}
|
||||
|
||||
var now = (function(){
|
||||
|
@ -74,11 +74,11 @@ someFunction = window.probes.measure(someFunction, "someFunction");
|
|||
|
||||
return function() {
|
||||
var name = nameParam;
|
||||
if (typeof name == "function"){
|
||||
if (typeof name === "function"){
|
||||
name = nameParam(arguments);
|
||||
}
|
||||
var p = window.probes[name];
|
||||
var owner = start === null;
|
||||
var owner = (!start);
|
||||
|
||||
if (before) {
|
||||
// would like to avoid try catch so its optimised properly by chrome
|
||||
|
|
|
@ -40,15 +40,15 @@
|
|||
// };
|
||||
//
|
||||
// var elementMap = {};
|
||||
// $.each(elements, function(idx,e){
|
||||
// jQuery.each(elements, function(idx,e){
|
||||
// elementMap[e] = true;
|
||||
// });
|
||||
//
|
||||
// var scrubAttributes = function(e){
|
||||
// $.each(e.attributes, function(idx, attr){
|
||||
// jQuery.each(e.attributes, function(idx, attr){
|
||||
//
|
||||
// if($.inArray(attr.name, attributes.all) === -1 &&
|
||||
// $.inArray(attr.name, attributes[e.tagName.toLowerCase()]) === -1) {
|
||||
// if(jQuery.inArray(attr.name, attributes.all) === -1 &&
|
||||
// jQuery.inArray(attr.name, attributes[e.tagName.toLowerCase()]) === -1) {
|
||||
// e.removeAttribute(attr.name);
|
||||
// }
|
||||
// });
|
||||
|
@ -74,7 +74,7 @@
|
|||
// e.parentNode.removeChild(e);
|
||||
// }
|
||||
// else {
|
||||
// $.each(clean.children, function(idx, inner){
|
||||
// jQuery.each(clean.children, function(idx, inner){
|
||||
// scrubTree(inner);
|
||||
// });
|
||||
// }
|
||||
|
|
169
app/assets/javascripts/discourse/components/screen_track.js
Normal file
169
app/assets/javascripts/discourse/components/screen_track.js
Normal file
|
@ -0,0 +1,169 @@
|
|||
|
||||
/* We use this class to track how long posts in a topic are on the screen.
|
||||
*/
|
||||
|
||||
|
||||
/* This could be a potentially awesome metric to keep track of.
|
||||
*/
|
||||
|
||||
|
||||
(function() {
|
||||
|
||||
window.Discourse.ScreenTrack = Ember.Object.extend({
|
||||
/* Don't send events if we haven't scrolled in a long time
|
||||
*/
|
||||
|
||||
PAUSE_UNLESS_SCROLLED: 1000 * 60 * 3,
|
||||
/* After 6 minutes stop tracking read position on post
|
||||
*/
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
}).call(this);
|
|
@ -1,128 +0,0 @@
|
|||
# We use this class to track how long posts in a topic are on the screen.
|
||||
# This could be a potentially awesome metric to keep track of.
|
||||
window.Discourse.ScreenTrack = Ember.Object.extend
|
||||
|
||||
# Don't send events if we haven't scrolled in a long time
|
||||
PAUSE_UNLESS_SCROLLED: 1000*60*3
|
||||
|
||||
# After 6 minutes stop tracking read position on post
|
||||
MAX_TRACKING_TIME: 1000*60*6
|
||||
|
||||
totalTimings: {}
|
||||
|
||||
# Elements to track
|
||||
timings: {}
|
||||
topicTime: 0
|
||||
|
||||
cancelled: false
|
||||
|
||||
track: (elementId, postNumber) ->
|
||||
@timings["##{elementId}"] =
|
||||
time: 0
|
||||
postNumber: postNumber
|
||||
|
||||
guessedSeen: (postNumber) ->
|
||||
@highestSeen = postNumber if postNumber > (@highestSeen || 0)
|
||||
|
||||
# Reset our timers
|
||||
reset: ->
|
||||
@lastTick = new Date().getTime()
|
||||
@lastFlush = 0
|
||||
@cancelled = false
|
||||
|
||||
# Start tracking
|
||||
start: ->
|
||||
@reset()
|
||||
@lastScrolled = new Date().getTime()
|
||||
@interval = setInterval =>
|
||||
@tick()
|
||||
, 1000
|
||||
|
||||
# Cancel and eject any tracking we have buffered
|
||||
cancel: ->
|
||||
@cancelled = true
|
||||
@timings = {}
|
||||
@topicTime = 0
|
||||
clearInterval(@interval)
|
||||
@interval = null
|
||||
|
||||
# Stop tracking and flush buffered read records
|
||||
stop: ->
|
||||
clearInterval(@interval)
|
||||
@interval = null
|
||||
@flush()
|
||||
|
||||
scrolled: ->
|
||||
@lastScrolled = new Date().getTime()
|
||||
|
||||
flush: ->
|
||||
|
||||
return if @cancelled
|
||||
|
||||
# We don't log anything unless we're logged in
|
||||
return unless Discourse.get('currentUser')
|
||||
|
||||
newTimings = {}
|
||||
Object.values @timings, (timing) =>
|
||||
@totalTimings[timing.postNumber] ||= 0
|
||||
if timing.time > 0 and @totalTimings[timing.postNumber] < @MAX_TRACKING_TIME
|
||||
@totalTimings[timing.postNumber] += timing.time
|
||||
newTimings[timing.postNumber] = timing.time
|
||||
timing.time = 0
|
||||
|
||||
topicId = @get('topic_id')
|
||||
|
||||
highestSeenByTopic = Discourse.get('highestSeenByTopic')
|
||||
if (highestSeenByTopic[topicId] || 0) < @highestSeen
|
||||
highestSeenByTopic[topicId] = @highestSeen
|
||||
|
||||
|
||||
unless Object.isEmpty(newTimings)
|
||||
$.ajax '/topics/timings'
|
||||
data:
|
||||
timings: newTimings
|
||||
topic_time: @topicTime
|
||||
highest_seen: @highestSeen
|
||||
topic_id: topicId
|
||||
cache: false
|
||||
type: 'POST'
|
||||
headers:
|
||||
'X-SILENCE-LOGGER': 'true'
|
||||
@topicTime = 0
|
||||
|
||||
@lastFlush = 0
|
||||
|
||||
tick: ->
|
||||
|
||||
# If the user hasn't scrolled the browser in a long time, stop tracking time read
|
||||
sinceScrolled = new Date().getTime() - @lastScrolled
|
||||
if sinceScrolled > @PAUSE_UNLESS_SCROLLED
|
||||
@reset()
|
||||
return
|
||||
|
||||
diff = new Date().getTime() - @lastTick
|
||||
@lastFlush += diff
|
||||
@lastTick = new Date().getTime()
|
||||
|
||||
@flush() if @lastFlush > (Discourse.SiteSettings.flush_timings_secs * 1000)
|
||||
|
||||
# Don't track timings if we're not in focus
|
||||
return unless Discourse.get("hasFocus")
|
||||
|
||||
@topicTime += diff
|
||||
|
||||
docViewTop = $(window).scrollTop() + $('header').height()
|
||||
docViewBottom = docViewTop + $(window).height()
|
||||
|
||||
Object.keys @timings, (id) =>
|
||||
$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 <= docViewBottom) or (docViewTop <= elemBottom <= docViewBottom)
|
||||
timing = @timings[id]
|
||||
timing.time = timing.time + diff
|
||||
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue