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