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:
Robin Ward 2013-02-20 13:15:50 -05:00
parent 68ad545f0f
commit f661fa609e
407 changed files with 13226 additions and 8953 deletions

View file

@ -66,8 +66,6 @@ gem 'discourse_emoji', path: 'vendor/gems/discourse_emoji'
# in production environments by default. # in production environments by default.
# allow everywhere for now cause we are allowing asset debugging in prd # allow everywhere for now cause we are allowing asset debugging in prd
group :assets do group :assets do
gem 'coffee-rails'
gem 'coffee-script' # need this to compile coffee on the fly
gem 'sass' gem 'sass'
gem 'sass-rails' gem 'sass-rails'
gem 'turbo-sprockets-rails3' gem 'turbo-sprockets-rails3'
@ -79,6 +77,7 @@ group :test do
end end
group :test, :development do group :test, :development do
gem 'guard-jshint-on-rails'
gem 'certified' gem 'certified'
gem 'fabrication' gem 'fabrication'
gem 'guard-jasmine' gem 'guard-jasmine'

View file

@ -183,6 +183,9 @@ GEM
guard (>= 1.1.0) guard (>= 1.1.0)
multi_json multi_json
thor thor
guard-jshint-on-rails (0.0.2)
guard (>= 1.0.0)
jshint_on_rails (>= 1.0.2)
guard-rspec (2.4.0) guard-rspec (2.4.0)
guard (>= 1.1) guard (>= 1.1)
rspec (~> 2.11) rspec (~> 2.11)
@ -215,6 +218,7 @@ GEM
jquery-rails (2.2.0) jquery-rails (2.2.0)
railties (>= 3.0, < 5.0) railties (>= 3.0, < 5.0)
thor (>= 0.14, < 2.0) thor (>= 0.14, < 2.0)
jshint_on_rails (1.0.2)
json (1.7.7) json (1.7.7)
jwt (0.1.5) jwt (0.1.5)
multi_json (>= 1.0) multi_json (>= 1.0)
@ -453,8 +457,6 @@ DEPENDENCIES
binding_of_caller binding_of_caller
certified certified
clockwork clockwork
coffee-rails
coffee-script
discourse_emoji! discourse_emoji!
discourse_plugin! discourse_plugin!
em-redis em-redis
@ -466,6 +468,7 @@ DEPENDENCIES
fastimage fastimage
fog fog
guard-jasmine guard-jasmine
guard-jshint-on-rails
guard-rspec guard-rspec
guard-spork guard-spork
has_ip_address has_ip_address

View file

@ -22,9 +22,17 @@ else
jasmine_options[:server_timeout] = 300 jasmine_options[:server_timeout] = 300
end end
guard 'jasmine', jasmine_options do watch(%r{spec/javascripts/spec\.(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\.coffee|js|coffee)$}) watch(%r{spec/javascripts/.+_spec\.js$})
watch(%r{app/assets/javascripts/(.+?)\.(js\.coffee|js|coffee)$}) { "spec/javascripts" } 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 end
guard 'rspec', :focus_on_failed => true, :cli => "--drb" do guard 'rspec', :focus_on_failed => true, :cli => "--drb" do
@ -45,6 +53,7 @@ guard 'rspec', :focus_on_failed => true, :cli => "--drb" do
end end
module ::Guard module ::Guard
class AutoReload < ::Guard::Guard class AutoReload < ::Guard::Guard
@ -85,3 +94,5 @@ guard :autoreload do
watch(/\.sass\.erb$/) watch(/\.sass\.erb$/)
watch(/\.handlebars$/) watch(/\.handlebars$/)
end end

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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);

View file

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

View 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);

View file

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

View 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);

View file

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

View 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);

View file

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

View 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);

View file

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

View 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);

View file

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

View file

@ -0,0 +1,9 @@
(function() {
Discourse.AdminCustomizeRoute = Discourse.Route.extend({
model: function() {
return Discourse.SiteCustomization.findAll();
}
});
}).call(this);

View file

@ -1,2 +0,0 @@
Discourse.AdminCustomizeRoute = Discourse.Route.extend
model: -> Discourse.SiteCustomization.findAll()

View 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);

View file

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

View file

@ -0,0 +1,9 @@
(function() {
Discourse.AdminEmailLogsRoute = Discourse.Route.extend({
model: function() {
return Discourse.EmailLog.findAll();
}
});
}).call(this);

View file

@ -1,2 +0,0 @@
Discourse.AdminEmailLogsRoute = Discourse.Route.extend
model: -> Discourse.EmailLog.findAll()

View file

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

View file

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

View 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);

View file

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

View 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);

View file

@ -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'

View file

@ -0,0 +1,9 @@
(function() {
Discourse.AdminSiteSettingsRoute = Discourse.Route.extend({
model: function() {
return Discourse.SiteSetting.findAll();
}
});
}).call(this);

View file

@ -1,2 +0,0 @@
Discourse.AdminSiteSettingsRoute = Discourse.Route.extend
model: -> Discourse.SiteSetting.findAll()

View file

@ -0,0 +1,9 @@
(function() {
Discourse.AdminUserRoute = Discourse.Route.extend({
model: function(params) {
return Discourse.AdminUser.find(params.username);
}
});
}).call(this);

View file

@ -1,2 +0,0 @@
Discourse.AdminUserRoute = Discourse.Route.extend
model: (params) -> Discourse.AdminUser.find(params.username)

View file

@ -0,0 +1,9 @@
(function() {
Discourse.AdminUsersListActiveRoute = Discourse.Route.extend({
setupController: function(c) {
return this.controllerFor('adminUsersList').show('active');
}
});
}).call(this);

View file

@ -1,2 +0,0 @@
Discourse.AdminUsersListActiveRoute = Discourse.Route.extend
setupController: (c) -> @controllerFor('adminUsersList').show('active')

View file

@ -0,0 +1,9 @@
(function() {
Discourse.AdminUsersListNewRoute = Discourse.Route.extend({
setupController: function(c) {
return this.controllerFor('adminUsersList').show('new');
}
});
}).call(this);

View file

@ -1,2 +0,0 @@
Discourse.AdminUsersListNewRoute = Discourse.Route.extend
setupController: (c) -> @controllerFor('adminUsersList').show('new')

View file

@ -0,0 +1,9 @@
(function() {
Discourse.AdminUsersListNewRoute = Discourse.Route.extend({
setupController: function(c) {
return this.controllerFor('adminUsersList').show('pending');
}
});
}).call(this);

View file

@ -1,2 +0,0 @@
Discourse.AdminUsersListNewRoute = Discourse.Route.extend
setupController: (c) -> @controllerFor('adminUsersList').show('pending')

View file

@ -4,4 +4,4 @@
<% admin = SimplesIdeias::I18n.translation_segments['app/assets/javascripts/i18n/admin.en.js'] <% admin = SimplesIdeias::I18n.translation_segments['app/assets/javascripts/i18n/admin.en.js']
admin[:en][:js] = admin[:en].delete(:admin_js) admin[:en][:js] = admin[:en].delete(:admin_js)
%> %>
$.extend(true, I18n.translations, <%= admin.to_json %>); jQuery.extend(true, I18n.translations, <%= admin.to_json %>);

View 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);

View file

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

View 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);

View file

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

View file

@ -0,0 +1,7 @@
(function() {
Discourse.AdminDashboardView = window.Discourse.View.extend({
templateName: 'admin/templates/dashboard'
});
}).call(this);

View file

@ -1,2 +0,0 @@
Discourse.AdminDashboardView = window.Discourse.View.extend
templateName: 'admin/templates/dashboard'

View file

@ -0,0 +1,7 @@
(function() {
Discourse.AdminEmailLogsView = window.Discourse.View.extend({
templateName: 'admin/templates/email_logs'
});
}).call(this);

View file

@ -1,2 +0,0 @@
Discourse.AdminEmailLogsView = window.Discourse.View.extend
templateName: 'admin/templates/email_logs'

View file

@ -0,0 +1,7 @@
(function() {
Discourse.AdminFlagsView = window.Discourse.View.extend({
templateName: 'admin/templates/flags'
});
}).call(this);

View file

@ -1,3 +0,0 @@
Discourse.AdminFlagsView = window.Discourse.View.extend
templateName: 'admin/templates/flags'

View file

@ -0,0 +1,7 @@
(function() {
Discourse.AdminSiteSettingsView = window.Discourse.View.extend({
templateName: 'admin/templates/site_settings'
});
}).call(this);

View file

@ -1,2 +0,0 @@
Discourse.AdminSiteSettingsView = window.Discourse.View.extend
templateName: 'admin/templates/site_settings'

View file

@ -0,0 +1,7 @@
(function() {
Discourse.AdminUserView = window.Discourse.View.extend({
templateName: 'admin/templates/user'
});
}).call(this);

View file

@ -1,2 +0,0 @@
Discourse.AdminUserView = window.Discourse.View.extend
templateName: 'admin/templates/user'

View file

@ -0,0 +1,7 @@
(function() {
Discourse.AdminUsersListView = window.Discourse.View.extend({
templateName: 'admin/templates/users_list'
});
}).call(this);

View file

@ -1,2 +0,0 @@
Discourse.AdminUsersListView = window.Discourse.View.extend
templateName: 'admin/templates/users_list'

View file

@ -0,0 +1,7 @@
(function() {
Discourse.AdminView = window.Discourse.View.extend({
templateName: 'admin/templates/admin'
});
}).call(this);

View file

@ -1,2 +0,0 @@
Discourse.AdminView = window.Discourse.View.extend
templateName: 'admin/templates/admin'

View file

@ -1,5 +1,5 @@
// This is a manifest file that'll be compiled into including all the files listed below. // 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 // 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 // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// the compiled file. // the compiled file.

View file

@ -159,7 +159,7 @@ var EXTRA_PARENT_PATHS_RE = /^(?:\.\.\/)*(?:\.\.$)?/;
* } * }
*/ */
function collapse_dots(path) { function collapse_dots(path) {
if (path === null) { return null; } if (path == null) { return null; }
var p = normPath(path); var p = normPath(path);
// Only /../ left to flatten // Only /../ left to flatten
var r = PARENT_DIRECTORY_HANDLER_RE; var r = PARENT_DIRECTORY_HANDLER_RE;
@ -1875,7 +1875,7 @@ var html = (function(html4) {
var parts = []; var parts = [];
var lastPos = 0; var lastPos = 0;
var m; var m;
while ((m = re.exec(str)) !== null) { while ((m = re.exec(str)) != null) {
parts.push(str.substring(lastPos, m.index)); parts.push(str.substring(lastPos, m.index));
parts.push(m[0]); parts.push(m[0]);
lastPos = m.index + m[0].length; 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) { for (var i = 0, n = attribs.length; i < n; i += 2) {
var attribName = attribs[i], var attribName = attribs[i],
value = attribs[i + 1]; value = attribs[i + 1];
if (value !== null && value !== void 0) { if (value != null && value !== void 0) {
out.push(' ', attribName, '="', escapeAttrib(value), '"'); out.push(' ', attribName, '="', escapeAttrib(value), '"');
} }
} }
@ -2241,7 +2241,7 @@ var html = (function(html4) {
html4.ATTRIBS.hasOwnProperty(attribKey))) { html4.ATTRIBS.hasOwnProperty(attribKey))) {
atype = html4.ATTRIBS[attribKey]; atype = html4.ATTRIBS[attribKey];
} }
if (atype !== null) { if (atype != null) {
switch (atype) { switch (atype) {
case html4.atype['NONE']: break; case html4.atype['NONE']: break;
case html4.atype['SCRIPT']: case html4.atype['SCRIPT']:
@ -2318,7 +2318,7 @@ var html = (function(html4) {
if (value && '#' === value.charAt(0)) { if (value && '#' === value.charAt(0)) {
value = value.substring(1); // remove the leading '#' value = value.substring(1); // remove the leading '#'
value = opt_nmTokenPolicy ? opt_nmTokenPolicy(value) : value; value = opt_nmTokenPolicy ? opt_nmTokenPolicy(value) : value;
if (value !== null && value !== void 0) { if (value != null && value !== void 0) {
value = '#' + value; // restore the leading '#' value = '#' + value; // restore the leading '#'
} }
} else { } else {

View 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);

View file

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

View 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);

View file

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

View 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);

View file

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

View 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, "&lt;");
buf = buf.replace(/>/g, "&gt;");
buf = buf.replace(/[ ]/g, "&#x200b;&nbsp;&#x200b;");
return buf.replace(/\n/g, "<br />");
};
makeCursor = function(pos, klass, color) {
var l;
l = val.substring(pos, pos + 1);
if (l === "\n") {
return "<br>";
}
return "<span class='" + klass + "' style='background-color:" + color + "; margin:0; padding: 0'>" + guard(l) + "</span>";
};
html = "";
if (before >= 0) {
html += guard(val.substring(0, pos - 1)) + makeCursor(before, "before", "#d0ffff");
if (insertSpaceAfterBefore) {
html += makeCursor(0, "post-before", "#d0ffff");
}
}
if (after >= 0) {
html += makeCursor(after, "after", "#ffd0ff");
if (after - 1 < val.length) {
html += guard(val.substring(after + 1));
}
}
p.html(html);
clone.scrollTop(textarea.scrollTop());
letter = p.find("span:first");
pos = letter.offset();
if (letter.hasClass("before")) {
pos.left = pos.left + letter.width();
}
pPos = p.offset();
return {
/*clone.hide().remove()
*/
left: pos.left - pPos.left,
top: (pos.top - pPos.top) - clone.scrollTop()
};
};
return $.fn.caretPosition;
})(jQuery);
}).call(this);

View file

@ -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,"&lt;")
buf = buf.replace(/>/g,"&gt;")
buf = buf.replace(/[ ]/g, "&#x200b;&nbsp;&#x200b;")
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

View 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);

View file

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

View 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);

View file

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

View file

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

View file

@ -1,7 +0,0 @@
Discourse.TextField = Ember.TextField.extend
attributeBindings: ['autocorrect', 'autocapitalize']
placeholder: (->
Em.String.i18n(@get('placeholderKey'))
).property('placeholderKey')

View 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);

View file

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

View file

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

View 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);

View file

@ -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]
)()

View file

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

View 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);

View file

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

View 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);

View file

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

View file

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

View file

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

View file

@ -51,19 +51,19 @@ someFunction = window.probes.measure(someFunction, "someFunction");
} }
else else
{ {
nameParam = options["name"]; nameParam = options.name;
if (nameParam === "measure" || nameParam == "clear") { if (nameParam === "measure" || nameParam === "clear") {
throw Error("can not be called measure or clear"); throw new Error("can not be called measure or clear");
} }
if (!nameParam) 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"]; before = options.before;
after = options["after"]; after = options.after;
} }
var now = (function(){ var now = (function(){
@ -74,11 +74,11 @@ someFunction = window.probes.measure(someFunction, "someFunction");
return function() { return function() {
var name = nameParam; var name = nameParam;
if (typeof name == "function"){ if (typeof name === "function"){
name = nameParam(arguments); name = nameParam(arguments);
} }
var p = window.probes[name]; var p = window.probes[name];
var owner = start === null; var owner = (!start);
if (before) { if (before) {
// would like to avoid try catch so its optimised properly by chrome // would like to avoid try catch so its optimised properly by chrome

View file

@ -40,15 +40,15 @@
// }; // };
// //
// var elementMap = {}; // var elementMap = {};
// $.each(elements, function(idx,e){ // jQuery.each(elements, function(idx,e){
// elementMap[e] = true; // elementMap[e] = true;
// }); // });
// //
// var scrubAttributes = function(e){ // var scrubAttributes = function(e){
// $.each(e.attributes, function(idx, attr){ // jQuery.each(e.attributes, function(idx, attr){
// //
// if($.inArray(attr.name, attributes.all) === -1 && // if(jQuery.inArray(attr.name, attributes.all) === -1 &&
// $.inArray(attr.name, attributes[e.tagName.toLowerCase()]) === -1) { // jQuery.inArray(attr.name, attributes[e.tagName.toLowerCase()]) === -1) {
// e.removeAttribute(attr.name); // e.removeAttribute(attr.name);
// } // }
// }); // });
@ -74,7 +74,7 @@
// e.parentNode.removeChild(e); // e.parentNode.removeChild(e);
// } // }
// else { // else {
// $.each(clean.children, function(idx, inner){ // jQuery.each(clean.children, function(idx, inner){
// scrubTree(inner); // scrubTree(inner);
// }); // });
// } // }

View 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);

View file

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